Skip to content

Commit 5488547

Browse files
nullvariantclaude
andcommitted
test: add moveIdentityInConfig and move button E2E tests
- Add 9 E2E tests for moveIdentityInConfig(): normal move up/down, boundary (first up, last down), single element, error cases (non-existent ID, invalid format), and persistence verification - Add 2 E2E tests for moveUp/moveDown button actions in showManageIdentitiesQuickPick() - Extend createManageMockVSCode to support moveUp/moveDown triggers - Update test counts in file headers - Bump version to 0.16.4 - Update CHANGELOG.md with Phase 5.7 feature summary 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Model-Raw: claude-opus-4-5-20251101
1 parent a501ce9 commit 5488547

5 files changed

Lines changed: 327 additions & 7 deletions

File tree

extensions/git-id-switcher/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.16.4] - 2026-01-29
11+
12+
### Added
13+
14+
- **Profile reorder (Move up / Move down) feature**:
15+
- Added `moveIdentityInConfig()` function to reorder identity profiles
16+
- Added inline move up / move down buttons in Manage Profiles UI
17+
- Added `handleManageMove()` command handler with focus management
18+
- Boundary handling: first item cannot move up, last item cannot move down
19+
- Security: ID format validation and audit logging for config changes
20+
- i18n: Move up / Move down / Failed to reorder tooltips (17 languages)
21+
- Allow ID editing when only one profile exists (`EditableFieldOrId` type)
22+
- Fix Codicon display in Edit Identity title
23+
24+
### Tests
25+
26+
- Added 9 E2E tests for `moveIdentityInConfig()`: normal move, boundary, single-element, error, and persistence
27+
- Added 2 E2E tests for moveUp/moveDown button actions in `showManageIdentitiesQuickPick()`
28+
1029
## [0.16.3] - 2026-01-28
1130

1231
### Changed

extensions/git-id-switcher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "git-id-switcher",
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
5-
"version": "0.16.3",
5+
"version": "0.16.4",
66
"publisher": "nullvariant",
77
"icon": "images/icon.png",
88
"engines": {

extensions/git-id-switcher/src/test/e2e/identity.test.ts

Lines changed: 261 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
* - deleteIdentityFromConfig: deletion and validation tests
1616
* - addIdentityToConfig: security validation and business logic tests
1717
* - updateIdentityInConfig: security validation and business logic tests
18+
* - moveIdentityInConfig: reorder, boundary, single-element, and persistence tests
1819
*
19-
* Test Count: 48 tests covering identity.ts related functionality
20+
* Test Count: 57 tests covering identity.ts related functionality
2021
*
2122
* Note: These tests use the real VS Code API (no mocks) to ensure actual behavior.
2223
* Each test restores original configuration values after execution.
@@ -28,6 +29,7 @@ import {
2829
deleteIdentityFromConfig,
2930
addIdentityToConfig,
3031
updateIdentityInConfig,
32+
moveIdentityInConfig,
3133
} from '../../identity/identity';
3234

3335
const EXTENSION_ID = 'nullvariant.git-id-switcher';
@@ -1231,4 +1233,262 @@ describe('Identity E2E Test Suite', function () {
12311233
});
12321234
});
12331235
});
1236+
1237+
// ===========================================================================
1238+
// 5.7.14-18 moveIdentityInConfig() Tests
1239+
// ===========================================================================
1240+
1241+
describe('moveIdentityInConfig', () => {
1242+
it('should move identity down successfully', async () => {
1243+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
1244+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
1245+
1246+
// Set up three identities
1247+
const testIdentities: TestIdentity[] = [
1248+
{ id: 'move-a', name: 'User A', email: 'a@example.com' },
1249+
{ id: 'move-b', name: 'User B', email: 'b@example.com' },
1250+
{ id: 'move-c', name: 'User C', email: 'c@example.com' },
1251+
];
1252+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
1253+
1254+
// Move first identity down
1255+
const result = await moveIdentityInConfig('move-a', 'down');
1256+
1257+
assert.strictEqual(result, true, 'Should return true for successful move');
1258+
1259+
// Verify new order: B, A, C
1260+
const freshConfig = vscode.workspace.getConfiguration(CONFIG_SECTION);
1261+
const afterMove = freshConfig.get<TestIdentity[]>('identities', []);
1262+
assert.strictEqual(afterMove[0].id, 'move-b', 'First should be B');
1263+
assert.strictEqual(afterMove[1].id, 'move-a', 'Second should be A');
1264+
assert.strictEqual(afterMove[2].id, 'move-c', 'Third should be C');
1265+
1266+
// Restore original state
1267+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
1268+
});
1269+
1270+
it('should move identity up successfully', async () => {
1271+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
1272+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
1273+
1274+
// Set up three identities
1275+
const testIdentities: TestIdentity[] = [
1276+
{ id: 'move-a', name: 'User A', email: 'a@example.com' },
1277+
{ id: 'move-b', name: 'User B', email: 'b@example.com' },
1278+
{ id: 'move-c', name: 'User C', email: 'c@example.com' },
1279+
];
1280+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
1281+
1282+
// Move last identity up
1283+
const result = await moveIdentityInConfig('move-c', 'up');
1284+
1285+
assert.strictEqual(result, true, 'Should return true for successful move');
1286+
1287+
// Verify new order: A, C, B
1288+
const freshConfig = vscode.workspace.getConfiguration(CONFIG_SECTION);
1289+
const afterMove = freshConfig.get<TestIdentity[]>('identities', []);
1290+
assert.strictEqual(afterMove[0].id, 'move-a', 'First should be A');
1291+
assert.strictEqual(afterMove[1].id, 'move-c', 'Second should be C');
1292+
assert.strictEqual(afterMove[2].id, 'move-b', 'Third should be B');
1293+
1294+
// Restore original state
1295+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
1296+
});
1297+
1298+
it('should return false when moving first item up (boundary)', async () => {
1299+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
1300+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
1301+
1302+
// Set up two identities
1303+
const testIdentities: TestIdentity[] = [
1304+
{ id: 'first-item', name: 'First User', email: 'first@example.com' },
1305+
{ id: 'second-item', name: 'Second User', email: 'second@example.com' },
1306+
];
1307+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
1308+
1309+
// Try to move first item up
1310+
const result = await moveIdentityInConfig('first-item', 'up');
1311+
1312+
assert.strictEqual(result, false, 'Should return false for boundary move');
1313+
1314+
// Verify order unchanged
1315+
const freshConfig = vscode.workspace.getConfiguration(CONFIG_SECTION);
1316+
const afterMove = freshConfig.get<TestIdentity[]>('identities', []);
1317+
assert.strictEqual(afterMove[0].id, 'first-item', 'First should remain first');
1318+
assert.strictEqual(afterMove[1].id, 'second-item', 'Second should remain second');
1319+
1320+
// Restore original state
1321+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
1322+
});
1323+
1324+
it('should return false when moving last item down (boundary)', async () => {
1325+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
1326+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
1327+
1328+
// Set up two identities
1329+
const testIdentities: TestIdentity[] = [
1330+
{ id: 'first-item', name: 'First User', email: 'first@example.com' },
1331+
{ id: 'last-item', name: 'Last User', email: 'last@example.com' },
1332+
];
1333+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
1334+
1335+
// Try to move last item down
1336+
const result = await moveIdentityInConfig('last-item', 'down');
1337+
1338+
assert.strictEqual(result, false, 'Should return false for boundary move');
1339+
1340+
// Verify order unchanged
1341+
const freshConfig = vscode.workspace.getConfiguration(CONFIG_SECTION);
1342+
const afterMove = freshConfig.get<TestIdentity[]>('identities', []);
1343+
assert.strictEqual(afterMove[0].id, 'first-item', 'First should remain first');
1344+
assert.strictEqual(afterMove[1].id, 'last-item', 'Last should remain last');
1345+
1346+
// Restore original state
1347+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
1348+
});
1349+
1350+
it('should return false when array has single element (move up)', async () => {
1351+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
1352+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
1353+
1354+
// Set up single identity
1355+
const testIdentities: TestIdentity[] = [
1356+
{ id: 'only-item', name: 'Only User', email: 'only@example.com' },
1357+
];
1358+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
1359+
1360+
// Try to move single item up
1361+
const result = await moveIdentityInConfig('only-item', 'up');
1362+
1363+
assert.strictEqual(result, false, 'Should return false for single element move up');
1364+
1365+
// Verify unchanged
1366+
const freshConfig = vscode.workspace.getConfiguration(CONFIG_SECTION);
1367+
const afterMove = freshConfig.get<TestIdentity[]>('identities', []);
1368+
assert.strictEqual(afterMove.length, 1, 'Should still have 1 identity');
1369+
assert.strictEqual(afterMove[0].id, 'only-item', 'Identity should be unchanged');
1370+
1371+
// Restore original state
1372+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
1373+
});
1374+
1375+
it('should return false when array has single element (move down)', async () => {
1376+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
1377+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
1378+
1379+
// Set up single identity
1380+
const testIdentities: TestIdentity[] = [
1381+
{ id: 'only-item', name: 'Only User', email: 'only@example.com' },
1382+
];
1383+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
1384+
1385+
// Try to move single item down
1386+
const result = await moveIdentityInConfig('only-item', 'down');
1387+
1388+
assert.strictEqual(result, false, 'Should return false for single element move down');
1389+
1390+
// Restore original state
1391+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
1392+
});
1393+
1394+
it('should throw error for non-existent identity ID', async () => {
1395+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
1396+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
1397+
1398+
// Set up known state
1399+
const testIdentities: TestIdentity[] = [
1400+
{ id: 'existing-move', name: 'Existing User', email: 'existing@example.com' },
1401+
];
1402+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
1403+
1404+
try {
1405+
await moveIdentityInConfig('non-existent-id', 'up');
1406+
assert.fail('Should have thrown an error');
1407+
} catch (error) {
1408+
assert.ok(error instanceof Error, 'Should throw an Error');
1409+
assert.ok(
1410+
error.message.includes('not found'),
1411+
`Error message should indicate identity not found, got: ${error.message}`
1412+
);
1413+
}
1414+
1415+
// Restore original state
1416+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
1417+
});
1418+
1419+
it('should throw error for invalid ID format', async () => {
1420+
const invalidIds = [
1421+
'',
1422+
'id with spaces',
1423+
'id@with@symbols',
1424+
'id/with/slashes',
1425+
];
1426+
1427+
for (const invalidId of invalidIds) {
1428+
try {
1429+
await moveIdentityInConfig(invalidId, 'up');
1430+
assert.fail(`Should have thrown an error for invalid ID: ${invalidId}`);
1431+
} catch (error) {
1432+
assert.ok(error instanceof Error, 'Should throw an Error');
1433+
assert.ok(
1434+
error.message.includes('Invalid'),
1435+
`Error message should indicate invalid ID format for "${invalidId}", got: ${error.message}`
1436+
);
1437+
}
1438+
}
1439+
});
1440+
1441+
it('should persist reorder to VS Code configuration (E2E)', async () => {
1442+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
1443+
const currentIdentities = config.get<TestIdentity[]>('identities', []);
1444+
1445+
// Set up identities with all fields to verify full data preservation
1446+
const testIdentities: TestIdentity[] = [
1447+
{
1448+
id: 'persist-a',
1449+
name: 'User A',
1450+
email: 'a@example.com',
1451+
icon: '🅰️',
1452+
service: 'GitHub',
1453+
},
1454+
{
1455+
id: 'persist-b',
1456+
name: 'User B',
1457+
email: 'b@example.com',
1458+
icon: '🅱️',
1459+
service: 'GitLab',
1460+
},
1461+
{
1462+
id: 'persist-c',
1463+
name: 'User C',
1464+
email: 'c@example.com',
1465+
},
1466+
];
1467+
await config.update('identities', testIdentities, vscode.ConfigurationTarget.Global);
1468+
1469+
// Move B up (swap A and B)
1470+
await moveIdentityInConfig('persist-b', 'up');
1471+
1472+
// Re-read configuration fresh to verify persistence
1473+
const freshConfig = vscode.workspace.getConfiguration(CONFIG_SECTION);
1474+
const persisted = freshConfig.get<TestIdentity[]>('identities', []);
1475+
1476+
// Verify order: B, A, C
1477+
assert.strictEqual(persisted.length, 3, 'Should still have 3 identities');
1478+
assert.strictEqual(persisted[0].id, 'persist-b', 'First should be B');
1479+
assert.strictEqual(persisted[1].id, 'persist-a', 'Second should be A');
1480+
assert.strictEqual(persisted[2].id, 'persist-c', 'Third should be C');
1481+
1482+
// Verify all data preserved after move
1483+
assert.strictEqual(persisted[0].name, 'User B', 'B name should be preserved');
1484+
assert.strictEqual(persisted[0].email, 'b@example.com', 'B email should be preserved');
1485+
assert.strictEqual(persisted[0].icon, '🅱️', 'B icon should be preserved');
1486+
assert.strictEqual(persisted[0].service, 'GitLab', 'B service should be preserved');
1487+
assert.strictEqual(persisted[1].name, 'User A', 'A name should be preserved');
1488+
assert.strictEqual(persisted[1].icon, '🅰️', 'A icon should be preserved');
1489+
1490+
// Restore original state
1491+
await config.update('identities', currentIdentities, vscode.ConfigurationTarget.Global);
1492+
});
1493+
});
12341494
});

extensions/git-id-switcher/src/test/e2e/identityPicker.test.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* - User Interaction: Selection, manage option, and cancellation
1818
* - Manage Identities UI: Inline buttons, back button, add option
1919
*
20-
* Test Count: 23 tests (9 for showIdentityQuickPick + 14 for showManageIdentitiesQuickPick)
20+
* Test Count: 25 tests (9 for showIdentityQuickPick + 16 for showManageIdentitiesQuickPick)
2121
*
2222
* Note: These tests use mocked VS Code API via vscodeLoader since quick pick
2323
* interactions require VS Code window API.
@@ -454,7 +454,7 @@ class MockThemeIcon {
454454
*/
455455
function createManageMockVSCode(options: {
456456
identities?: Identity[];
457-
triggerAction?: 'back' | 'add' | 'addButton' | 'edit' | 'delete' | 'selectIdentity' | 'selectPlaceholder' | 'hide';
457+
triggerAction?: 'back' | 'add' | 'addButton' | 'edit' | 'delete' | 'moveUp' | 'moveDown' | 'selectIdentity' | 'selectPlaceholder' | 'hide';
458458
actionIndex?: number;
459459
}) {
460460
let capturedItems: CapturedManageQuickPickItem[] = [];
@@ -546,13 +546,18 @@ function createManageMockVSCode(options: {
546546
if (buttonCallback && capturedButtons.length >= 2) {
547547
buttonCallback(capturedButtons[1]); // Add button
548548
}
549-
} else if (options.triggerAction === 'edit' || options.triggerAction === 'delete') {
549+
} else if (options.triggerAction === 'edit' || options.triggerAction === 'delete'
550+
|| options.triggerAction === 'moveUp' || options.triggerAction === 'moveDown') {
550551
// Trigger item button
551552
const idx = options.actionIndex ?? 0;
552553
const item = capturedItems.find(i => i.index === idx);
553554
if (item && item.buttons && itemButtonCallback) {
554555
// Find button by icon id
555-
const targetIconId = options.triggerAction === 'edit' ? 'pencil' : 'trash';
556+
const iconMap: Record<string, string> = {
557+
edit: 'pencil', delete: 'trash',
558+
moveUp: 'arrow-up', moveDown: 'arrow-down',
559+
};
560+
const targetIconId = iconMap[options.triggerAction];
556561
const button = item.buttons.find(
557562
b => (b.iconPath as MockThemeIcon)?.id === targetIconId
558563
);
@@ -798,6 +803,42 @@ describe('showManageIdentitiesQuickPick E2E Test Suite', function () {
798803
}
799804
});
800805

806+
it('should return moveUp action when move up button clicked', async () => {
807+
const mockVSCode = createManageMockVSCode({
808+
identities: [TEST_IDENTITIES.work, TEST_IDENTITIES.personal],
809+
triggerAction: 'moveUp',
810+
actionIndex: 1,
811+
});
812+
_setMockVSCode(mockVSCode as never);
813+
814+
const result = await showManageIdentitiesQuickPick([TEST_IDENTITIES.work, TEST_IDENTITIES.personal]);
815+
816+
assert.ok(result, 'Should return result');
817+
assert.strictEqual(result?.action, 'moveUp', 'Action should be moveUp');
818+
if (result?.action === 'moveUp') {
819+
assert.strictEqual(result.identity.id, 'personal', 'Should return correct identity');
820+
assert.strictEqual(result.index, 1, 'Should return correct index');
821+
}
822+
});
823+
824+
it('should return moveDown action when move down button clicked', async () => {
825+
const mockVSCode = createManageMockVSCode({
826+
identities: [TEST_IDENTITIES.work, TEST_IDENTITIES.personal],
827+
triggerAction: 'moveDown',
828+
actionIndex: 0,
829+
});
830+
_setMockVSCode(mockVSCode as never);
831+
832+
const result = await showManageIdentitiesQuickPick([TEST_IDENTITIES.work, TEST_IDENTITIES.personal]);
833+
834+
assert.ok(result, 'Should return result');
835+
assert.strictEqual(result?.action, 'moveDown', 'Action should be moveDown');
836+
if (result?.action === 'moveDown') {
837+
assert.strictEqual(result.identity.id, 'work', 'Should return correct identity');
838+
assert.strictEqual(result.index, 0, 'Should return correct index');
839+
}
840+
});
841+
801842
it('should return add action when add option selected', async () => {
802843
const mockVSCode = createManageMockVSCode({
803844
identities: [TEST_IDENTITIES.work],

0 commit comments

Comments
 (0)