mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-13 00:13:33 +08:00
✨ feat(mac-window): 支持切换 macOS 原生窗口控制与原生全屏行为
This commit is contained in:
@@ -1,69 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { calculateTableBodyBottomPadding, calculateVirtualTableScrollX } from './dataGridLayout';
|
||||
|
||||
const assertEqual = (actual: unknown, expected: unknown, message: string) => {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${message}\nactual: ${String(actual)}\nexpected: ${String(expected)}`);
|
||||
}
|
||||
};
|
||||
describe('dataGridLayout helpers', () => {
|
||||
it('returns zero bottom padding without horizontal overflow', () => {
|
||||
expect(calculateTableBodyBottomPadding({
|
||||
hasHorizontalOverflow: false,
|
||||
floatingScrollbarHeight: 10,
|
||||
floatingScrollbarGap: 6,
|
||||
})).toBe(0);
|
||||
});
|
||||
|
||||
assertEqual(
|
||||
calculateTableBodyBottomPadding({
|
||||
hasHorizontalOverflow: false,
|
||||
floatingScrollbarHeight: 10,
|
||||
floatingScrollbarGap: 6,
|
||||
}),
|
||||
0,
|
||||
'无横向滚动条时不应增加底部间距'
|
||||
);
|
||||
it('adds safe area when horizontal overflow exists', () => {
|
||||
expect(calculateTableBodyBottomPadding({
|
||||
hasHorizontalOverflow: true,
|
||||
floatingScrollbarHeight: 10,
|
||||
floatingScrollbarGap: 6,
|
||||
})).toBe(28);
|
||||
expect(calculateTableBodyBottomPadding({
|
||||
hasHorizontalOverflow: true,
|
||||
floatingScrollbarHeight: 14,
|
||||
floatingScrollbarGap: 4,
|
||||
})).toBe(30);
|
||||
});
|
||||
|
||||
assertEqual(
|
||||
calculateTableBodyBottomPadding({
|
||||
hasHorizontalOverflow: true,
|
||||
floatingScrollbarHeight: 10,
|
||||
floatingScrollbarGap: 6,
|
||||
}),
|
||||
28,
|
||||
'默认悬浮滚动条应预留滚动条高度、间距和额外安全区'
|
||||
);
|
||||
|
||||
assertEqual(
|
||||
calculateTableBodyBottomPadding({
|
||||
hasHorizontalOverflow: true,
|
||||
floatingScrollbarHeight: 14,
|
||||
floatingScrollbarGap: 4,
|
||||
}),
|
||||
30,
|
||||
'较粗滚动条场景下应同步放大底部安全区'
|
||||
);
|
||||
|
||||
assertEqual(
|
||||
calculateVirtualTableScrollX({
|
||||
totalWidth: 646,
|
||||
tableViewportWidth: 1200,
|
||||
isMacLike: false,
|
||||
}),
|
||||
1200,
|
||||
'列总宽小于视口时应按视口宽度返回 scroll.x,避免 header/body 走两套宽度'
|
||||
);
|
||||
|
||||
assertEqual(
|
||||
calculateVirtualTableScrollX({
|
||||
totalWidth: 646,
|
||||
tableViewportWidth: 0,
|
||||
isMacLike: false,
|
||||
}),
|
||||
646,
|
||||
'未拿到视口宽度时应退回列宽总和'
|
||||
);
|
||||
|
||||
assertEqual(
|
||||
calculateVirtualTableScrollX({
|
||||
totalWidth: 1200,
|
||||
tableViewportWidth: 800,
|
||||
isMacLike: true,
|
||||
}),
|
||||
1202,
|
||||
'macOS 横向溢出时仍需额外预留 2px 以稳定滚动轨道'
|
||||
);
|
||||
|
||||
console.log('dataGridLayout tests passed');
|
||||
it('keeps scroll width aligned with viewport or content width', () => {
|
||||
expect(calculateVirtualTableScrollX({ totalWidth: 646, tableViewportWidth: 1200, isMacLike: false })).toBe(1200);
|
||||
expect(calculateVirtualTableScrollX({ totalWidth: 646, tableViewportWidth: 0, isMacLike: false })).toBe(646);
|
||||
expect(calculateVirtualTableScrollX({ totalWidth: 1200, tableViewportWidth: 800, isMacLike: true })).toBe(1202);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { RedisKeyInfo } from '../types';
|
||||
import {
|
||||
applyRenamedRedisKeyState,
|
||||
@@ -7,20 +9,6 @@ import {
|
||||
isGroupFullyChecked,
|
||||
} from './redisViewerTree';
|
||||
|
||||
const assert = (condition: unknown, message: string) => {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const assertEqual = (actual: unknown, expected: unknown, message: string) => {
|
||||
const actualText = JSON.stringify(actual);
|
||||
const expectedText = JSON.stringify(expected);
|
||||
if (actualText !== expectedText) {
|
||||
throw new Error(`${message}\nactual: ${actualText}\nexpected: ${expectedText}`);
|
||||
}
|
||||
};
|
||||
|
||||
const sampleKeys: RedisKeyInfo[] = [
|
||||
{ key: 'app:user:1', type: 'string', ttl: -1 },
|
||||
{ key: 'app:user:2', type: 'string', ttl: -1 },
|
||||
@@ -28,78 +16,64 @@ const sampleKeys: RedisKeyInfo[] = [
|
||||
{ key: 'misc', type: 'set', ttl: -1 },
|
||||
];
|
||||
|
||||
const tree = buildRedisKeyTree(sampleKeys, true);
|
||||
const appGroup = tree.treeData.find((node) => node.key === 'group:app');
|
||||
const userGroup = appGroup?.children?.find((node) => node.key === 'group:app:user');
|
||||
describe('redisViewerTree helpers', () => {
|
||||
it('builds grouped redis key tree and group selection state', () => {
|
||||
const tree = buildRedisKeyTree(sampleKeys, true);
|
||||
const appGroup = tree.treeData.find((node) => node.key === 'group:app');
|
||||
const userGroup = appGroup?.children?.find((node) => node.key === 'group:app:user');
|
||||
|
||||
assert(appGroup, '应生成 group:app 节点');
|
||||
assert(userGroup, '应生成 group:app:user 节点');
|
||||
assertEqual(
|
||||
appGroup?.descendantRawKeys,
|
||||
['app:order:1', 'app:user:1', 'app:user:2'],
|
||||
'app 分组应收集全部后代 key'
|
||||
);
|
||||
expect(appGroup).toBeTruthy();
|
||||
expect(userGroup).toBeTruthy();
|
||||
expect(appGroup?.descendantRawKeys).toEqual(['app:order:1', 'app:user:1', 'app:user:2']);
|
||||
|
||||
const selectedAfterGroupCheck = applyTreeNodeCheck([], appGroup!, true);
|
||||
assertEqual(
|
||||
selectedAfterGroupCheck,
|
||||
['app:order:1', 'app:user:1', 'app:user:2'],
|
||||
'勾选分组应递归选中全部后代 key'
|
||||
);
|
||||
const selectedAfterGroupCheck = applyTreeNodeCheck([], appGroup!, true);
|
||||
expect(selectedAfterGroupCheck).toEqual(['app:order:1', 'app:user:1', 'app:user:2']);
|
||||
|
||||
const checkedState = buildCheckedTreeNodeState(selectedAfterGroupCheck, tree);
|
||||
assertEqual(
|
||||
checkedState.checked,
|
||||
['key:app:order:1', 'group:app:order', 'key:app:user:1', 'key:app:user:2', 'group:app:user', 'group:app'],
|
||||
'全部后代已选中时,父分组和叶子都应进入 checked'
|
||||
);
|
||||
assertEqual(checkedState.halfChecked, [], '全部后代已选中时不应有 halfChecked');
|
||||
assertEqual(isGroupFullyChecked(appGroup!, selectedAfterGroupCheck), true, '全部后代已选中时,分组应视为 fully checked');
|
||||
const checkedState = buildCheckedTreeNodeState(selectedAfterGroupCheck, tree);
|
||||
expect(checkedState.checked).toEqual(['key:app:order:1', 'group:app:order', 'key:app:user:1', 'key:app:user:2', 'group:app:user', 'group:app']);
|
||||
expect(checkedState.halfChecked).toEqual([]);
|
||||
expect(isGroupFullyChecked(appGroup!, selectedAfterGroupCheck)).toBe(true);
|
||||
|
||||
const selectedAfterGroupUncheck = applyTreeNodeCheck(selectedAfterGroupCheck, appGroup!, false);
|
||||
assertEqual(selectedAfterGroupUncheck, [], '取消勾选分组应移除全部后代 key');
|
||||
assertEqual(isGroupFullyChecked(appGroup!, selectedAfterGroupUncheck), false, '取消后分组不应再是 fully checked');
|
||||
const selectedAfterGroupUncheck = applyTreeNodeCheck(selectedAfterGroupCheck, appGroup!, false);
|
||||
expect(selectedAfterGroupUncheck).toEqual([]);
|
||||
expect(isGroupFullyChecked(appGroup!, selectedAfterGroupUncheck)).toBe(false);
|
||||
});
|
||||
|
||||
const partialState = buildCheckedTreeNodeState(['app:user:1'], tree);
|
||||
assertEqual(
|
||||
partialState.halfChecked,
|
||||
['group:app:user', 'group:app'],
|
||||
'仅部分后代选中时,相关分组应进入 halfChecked'
|
||||
);
|
||||
assertEqual(isGroupFullyChecked(appGroup!, ['app:user:1']), false, '部分选中时分组不应是 fully checked');
|
||||
it('marks parent groups as half checked for partial selection', () => {
|
||||
const tree = buildRedisKeyTree(sampleKeys, true);
|
||||
const appGroup = tree.treeData.find((node) => node.key === 'group:app');
|
||||
const partialState = buildCheckedTreeNodeState(['app:user:1'], tree);
|
||||
|
||||
const renamedState = applyRenamedRedisKeyState(
|
||||
{
|
||||
keys: sampleKeys,
|
||||
selectedKey: 'app:user:2',
|
||||
selectedKeys: ['app:user:1', 'app:user:2', 'misc'],
|
||||
},
|
||||
'app:user:2',
|
||||
'app:user:200'
|
||||
);
|
||||
expect(partialState.halfChecked).toEqual(['group:app:user', 'group:app']);
|
||||
expect(isGroupFullyChecked(appGroup!, ['app:user:1'])).toBe(false);
|
||||
});
|
||||
|
||||
assertEqual(
|
||||
renamedState.keys.map((item) => item.key),
|
||||
['app:user:1', 'app:user:200', 'app:order:1', 'misc'],
|
||||
'重命名后 keys 列表应替换旧 key'
|
||||
);
|
||||
assertEqual(renamedState.selectedKey, 'app:user:200', '当前详情选中的 key 应切换为新 key');
|
||||
assertEqual(
|
||||
renamedState.selectedKeys,
|
||||
['app:user:1', 'app:user:200', 'misc'],
|
||||
'批量选中集合中的旧 key 应映射为新 key'
|
||||
);
|
||||
it('updates selected keys consistently after rename', () => {
|
||||
const renamedState = applyRenamedRedisKeyState(
|
||||
{
|
||||
keys: sampleKeys,
|
||||
selectedKey: 'app:user:2',
|
||||
selectedKeys: ['app:user:1', 'app:user:2', 'misc'],
|
||||
},
|
||||
'app:user:2',
|
||||
'app:user:200'
|
||||
);
|
||||
|
||||
const unrelatedRenameState = applyRenamedRedisKeyState(
|
||||
{
|
||||
keys: sampleKeys,
|
||||
selectedKey: 'misc',
|
||||
selectedKeys: ['app:user:1'],
|
||||
},
|
||||
'app:order:1',
|
||||
'app:order:9'
|
||||
);
|
||||
assertEqual(unrelatedRenameState.selectedKey, 'misc', '非当前详情 key 的重命名不应影响 selectedKey');
|
||||
assertEqual(unrelatedRenameState.selectedKeys, ['app:user:1'], '非已勾选 key 的重命名不应污染选中集合');
|
||||
expect(renamedState.keys.map((item) => item.key)).toEqual(['app:user:1', 'app:user:200', 'app:order:1', 'misc']);
|
||||
expect(renamedState.selectedKey).toBe('app:user:200');
|
||||
expect(renamedState.selectedKeys).toEqual(['app:user:1', 'app:user:200', 'misc']);
|
||||
|
||||
console.log('redisViewerTree tests passed');
|
||||
const unrelatedRenameState = applyRenamedRedisKeyState(
|
||||
{
|
||||
keys: sampleKeys,
|
||||
selectedKey: 'misc',
|
||||
selectedKeys: ['app:user:1'],
|
||||
},
|
||||
'app:order:1',
|
||||
'app:order:9'
|
||||
);
|
||||
|
||||
expect(unrelatedRenameState.selectedKey).toBe('misc');
|
||||
expect(unrelatedRenameState.selectedKeys).toEqual(['app:user:1']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,50 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildRedisWorkbenchTheme } from './redisViewerWorkbenchTheme';
|
||||
|
||||
const assertEqual = (actual: unknown, expected: unknown, message: string) => {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${message}\nactual: ${String(actual)}\nexpected: ${String(expected)}`);
|
||||
}
|
||||
};
|
||||
describe('buildRedisWorkbenchTheme', () => {
|
||||
it('builds dark redis workbench theme', () => {
|
||||
const darkTheme = buildRedisWorkbenchTheme({ darkMode: true, opacity: 0.72, blur: 14 });
|
||||
expect(darkTheme.isDark).toBe(true);
|
||||
expect(darkTheme.panelBg).toMatch(/^rgba\(/);
|
||||
expect(darkTheme.toolbarPrimaryBg).toMatch(/^linear-gradient\(/);
|
||||
expect(darkTheme.actionDangerBg).not.toBe(darkTheme.actionSecondaryBg);
|
||||
expect(darkTheme.treeSelectedBg).not.toBe(darkTheme.treeHoverBg);
|
||||
expect(darkTheme.appBg).toMatch(/rgba\(15, 15, 17,/);
|
||||
expect(darkTheme.panelBg).toMatch(/rgba\(24, 24, 28,/);
|
||||
expect(darkTheme.panelBgStrong).toMatch(/rgba\(31, 31, 36,/);
|
||||
expect(darkTheme.backdropFilter).toBe('blur(14px)');
|
||||
});
|
||||
|
||||
const assertNotEqual = (actual: unknown, expected: unknown, message: string) => {
|
||||
if (actual === expected) {
|
||||
throw new Error(`${message}\nactual: ${String(actual)}\nnotExpected: ${String(expected)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const assertMatch = (value: string, pattern: RegExp, message: string) => {
|
||||
if (!pattern.test(value)) {
|
||||
throw new Error(`${message}\nactual: ${value}\npattern: ${String(pattern)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const darkTheme = buildRedisWorkbenchTheme({
|
||||
darkMode: true,
|
||||
opacity: 0.72,
|
||||
blur: 14,
|
||||
it('builds light redis workbench theme', () => {
|
||||
const lightTheme = buildRedisWorkbenchTheme({ darkMode: false, opacity: 1, blur: 0 });
|
||||
expect(lightTheme.isDark).toBe(false);
|
||||
expect(lightTheme.panelBg).toMatch(/^rgba\(/);
|
||||
expect(lightTheme.contentEmptyBg).toMatch(/^linear-gradient\(/);
|
||||
expect(lightTheme.textPrimary).not.toBe(lightTheme.textSecondary);
|
||||
expect(lightTheme.statusTagBg).not.toBe(lightTheme.statusTagMutedBg);
|
||||
expect(lightTheme.backdropFilter).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
assertEqual(darkTheme.isDark, true, 'dark 主题标记应为 true');
|
||||
assertMatch(darkTheme.panelBg, /^rgba\(/, 'dark 主题面板背景应为 rgba');
|
||||
assertMatch(darkTheme.toolbarPrimaryBg, /^linear-gradient\(/, '工具栏主按钮应使用渐变背景');
|
||||
assertNotEqual(darkTheme.actionDangerBg, darkTheme.actionSecondaryBg, '危险态按钮背景不应与普通按钮相同');
|
||||
assertNotEqual(darkTheme.treeSelectedBg, darkTheme.treeHoverBg, '树节点选中态与悬浮态不应相同');
|
||||
assertMatch(darkTheme.appBg, /rgba\(15, 15, 17,/, 'dark 背景应保持中性黑基底');
|
||||
assertMatch(darkTheme.panelBg, /rgba\(24, 24, 28,/, 'dark 面板背景应保持中性黑灰');
|
||||
assertMatch(darkTheme.panelBgStrong, /rgba\(31, 31, 36,/, 'dark 强面板背景应保持中性黑灰');
|
||||
assertEqual(darkTheme.backdropFilter, 'blur(14px)', 'blur 参数应映射为 backdropFilter');
|
||||
|
||||
const lightTheme = buildRedisWorkbenchTheme({
|
||||
darkMode: false,
|
||||
opacity: 1,
|
||||
blur: 0,
|
||||
});
|
||||
|
||||
assertEqual(lightTheme.isDark, false, 'light 主题标记应为 false');
|
||||
assertMatch(lightTheme.panelBg, /^rgba\(/, 'light 主题面板背景应为 rgba');
|
||||
assertMatch(lightTheme.contentEmptyBg, /^linear-gradient\(/, 'light 空状态背景应为渐变');
|
||||
assertNotEqual(lightTheme.textPrimary, lightTheme.textSecondary, '主次文本颜色应区分');
|
||||
assertNotEqual(lightTheme.statusTagBg, lightTheme.statusTagMutedBg, '状态 tag 应区分普通与弱化样式');
|
||||
assertEqual(lightTheme.backdropFilter, 'none', 'blur=0 时 backdropFilter 应为 none');
|
||||
|
||||
console.log('redisViewerWorkbenchTheme tests passed');
|
||||
|
||||
Reference in New Issue
Block a user