Merge remote-tracking branch 'origin/main' into koesie10/variant-analysis-remove-item-tests

This commit is contained in:
Koen Vlaswinkel
2022-12-09 09:47:46 +01:00
31 changed files with 1418 additions and 451 deletions

View File

@@ -47,7 +47,7 @@ jobs:
cp dist/*.vsix artifacts
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: matrix.os == 'ubuntu-latest'
with:
name: vscode-codeql-extension

29
.vscode/settings.json vendored
View File

@@ -42,13 +42,28 @@
"LANG": "en-US",
"TZ": "UTC"
},
// Uncomment to debug integration tests
// "jestrunner.debugOptions": {
// "attachSimplePort": 9223,
// "env": {
// "VSCODE_WAIT_FOR_DEBUGGER": "true",
// }
// },
"jestrunner.debugOptions": {
// Uncomment to debug integration tests
// "attachSimplePort": 9223,
"env": {
"LANG": "en-US",
"TZ": "UTC",
// Uncomment to debug integration tests
// "VSCODE_WAIT_FOR_DEBUGGER": "true",
}
},
"terminal.integrated.env.linux": {
"LANG": "en-US",
"TZ": "UTC"
},
"terminal.integrated.env.osx": {
"LANG": "en-US",
"TZ": "UTC"
},
"terminal.integrated.env.windows": {
"LANG": "en-US",
"TZ": "UTC"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,

View File

@@ -105,6 +105,7 @@
"ansi-colors": "^4.1.1",
"applicationinsights": "^2.3.5",
"babel-loader": "^8.2.5",
"cross-env": "^7.0.3",
"css-loader": "~3.1.0",
"del": "^6.0.0",
"eslint": "^8.23.1",
@@ -18780,6 +18781,83 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-env/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/cross-env/node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/cross-env/node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cross-env/node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/cross-env/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
@@ -55164,6 +55242,58 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.1"
},
"dependencies": {
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",

View File

@@ -1286,7 +1286,7 @@
"watch:webpack": "gulp watchView",
"watch:files": "gulp watchTestData",
"test": "npm-run-all -p test:*",
"test:unit": "jest --projects test",
"test:unit": "cross-env TZ=UTC LANG=en-US jest --projects test",
"test:view": "jest --projects src/view",
"integration": "npm-run-all integration:*",
"integration:no-workspace": "jest --projects src/vscode-tests/no-workspace",
@@ -1397,6 +1397,7 @@
"ansi-colors": "^4.1.1",
"applicationinsights": "^2.3.5",
"babel-loader": "^8.2.5",
"cross-env": "^7.0.3",
"css-loader": "~3.1.0",
"del": "^6.0.0",
"eslint": "^8.23.1",

View File

@@ -1,3 +1,29 @@
diff --git a/node_modules/jest-runner-vscode/dist/child/environment.js b/node_modules/jest-runner-vscode/dist/child/environment.js
index 1ac28d5..f91f216 100644
--- a/node_modules/jest-runner-vscode/dist/child/environment.js
+++ b/node_modules/jest-runner-vscode/dist/child/environment.js
@@ -10,6 +10,21 @@ const wrap_io_1 = __importDefault(require("./wrap-io"));
const load_pnp_1 = __importDefault(require("./load-pnp"));
const ipc = new ipc_client_1.default('env');
class VSCodeEnvironment extends jest_environment_node_1.default {
+ constructor(config, context) {
+ super(config, context);
+ // The _VSCODE_NODE_MODULES is a proxy which will require a module if any property
+ // on it is accessed. This is a workaround for the fact that jest will call
+ // _isMockFunction on the module, which will cause that function to be required.
+ this.global._VSCODE_NODE_MODULES = new Proxy(this.global._VSCODE_NODE_MODULES, {
+ get(target, prop) {
+ if (prop === '_isMockFunction') {
+ return undefined;
+ }
+ return target[prop];
+ },
+ });
+ }
+
async setup() {
await super.setup();
await (0, load_pnp_1.default)();
diff --git a/node_modules/jest-runner-vscode/dist/child/runner.js b/node_modules/jest-runner-vscode/dist/child/runner.js
index 0663c5c..4991663 100644
--- a/node_modules/jest-runner-vscode/dist/child/runner.js

View File

@@ -1,6 +1,11 @@
import { pathExists, writeJSON, readJSON, readJSONSync } from "fs-extra";
import { join } from "path";
import { cloneDbConfig, DbConfig, SelectedDbItem } from "./db-config";
import {
cloneDbConfig,
DbConfig,
ExpandedDbItem,
SelectedDbItem,
} from "./db-config";
import * as chokidar from "chokidar";
import { DisposableObject, DisposeHandler } from "../../pure/disposable-object";
import { DbConfigValidator } from "./db-config-validator";
@@ -72,6 +77,19 @@ export class DbConfigStore extends DisposableObject {
await this.writeConfig(config);
}
public async updateExpandedState(expandedItems: ExpandedDbItem[]) {
if (!this.config) {
throw Error("Cannot update expansion state if config is not loaded");
}
const config: DbConfig = {
...this.config,
expanded: expandedItems,
};
await this.writeConfig(config);
}
private async writeConfig(config: DbConfig): Promise<void> {
await writeJSON(this.configPath, config, {
spaces: 2,
@@ -137,6 +155,7 @@ export class DbConfigStore extends DisposableObject {
databases: [],
},
},
expanded: [],
};
}
}

View File

@@ -2,6 +2,7 @@
export interface DbConfig {
databases: DbConfigDatabases;
expanded: ExpandedDbItem[];
selected?: SelectedDbItem;
}
@@ -87,6 +88,37 @@ export interface LocalDatabase {
storagePath: string;
}
export type ExpandedDbItem =
| RootLocalExpandedDbItem
| LocalUserDefinedListExpandedDbItem
| RootRemoteExpandedDbItem
| RemoteUserDefinedListExpandedDbItem;
export enum ExpandedDbItemKind {
RootLocal = "rootLocal",
LocalUserDefinedList = "localUserDefinedList",
RootRemote = "rootRemote",
RemoteUserDefinedList = "remoteUserDefinedList",
}
export interface RootLocalExpandedDbItem {
kind: ExpandedDbItemKind.RootLocal;
}
export interface LocalUserDefinedListExpandedDbItem {
kind: ExpandedDbItemKind.LocalUserDefinedList;
listName: string;
}
export interface RootRemoteExpandedDbItem {
kind: ExpandedDbItemKind.RootRemote;
}
export interface RemoteUserDefinedListExpandedDbItem {
kind: ExpandedDbItemKind.RemoteUserDefinedList;
listName: string;
}
export function cloneDbConfig(config: DbConfig): DbConfig {
return {
databases: {
@@ -108,6 +140,7 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
databases: config.databases.local.databases.map((db) => ({ ...db })),
},
},
expanded: config.expanded.map(cloneDbConfigExpandedItem),
selected: config.selected
? cloneDbConfigSelectedItem(config.selected)
: undefined,
@@ -150,3 +183,17 @@ function cloneDbConfigSelectedItem(selected: SelectedDbItem): SelectedDbItem {
};
}
}
function cloneDbConfigExpandedItem(item: ExpandedDbItem): ExpandedDbItem {
switch (item.kind) {
case ExpandedDbItemKind.RootLocal:
case ExpandedDbItemKind.RootRemote:
return { kind: item.kind };
case ExpandedDbItemKind.LocalUserDefinedList:
case ExpandedDbItemKind.RemoteUserDefinedList:
return {
kind: item.kind,
listName: item.listName,
};
}
}

View File

@@ -0,0 +1,66 @@
import { ExpandedDbItem, ExpandedDbItemKind } from "./config/db-config";
import { DbItem, DbItemKind } from "./db-item";
export function calculateNewExpandedState(
currentExpandedItems: ExpandedDbItem[],
dbItem: DbItem,
itemExpanded: boolean,
): ExpandedDbItem[] {
if (itemExpanded) {
const expandedDbItem = mapDbItemToExpandedDbItem(dbItem);
const expandedItems = [...currentExpandedItems];
if (!expandedItems.some((i) => isDbItemEqualToExpandedDbItem(dbItem, i))) {
expandedItems.push(expandedDbItem);
}
return expandedItems;
} else {
return currentExpandedItems.filter(
(i) => !isDbItemEqualToExpandedDbItem(dbItem, i),
);
}
}
function mapDbItemToExpandedDbItem(dbItem: DbItem): ExpandedDbItem {
switch (dbItem.kind) {
case DbItemKind.RootLocal:
return { kind: ExpandedDbItemKind.RootLocal };
case DbItemKind.LocalList:
return {
kind: ExpandedDbItemKind.LocalUserDefinedList,
listName: dbItem.listName,
};
case DbItemKind.RootRemote:
return { kind: ExpandedDbItemKind.RootRemote };
case DbItemKind.RemoteUserDefinedList:
return {
kind: ExpandedDbItemKind.RemoteUserDefinedList,
listName: dbItem.listName,
};
default:
throw Error(`Unknown db item kind ${dbItem.kind}`);
}
}
function isDbItemEqualToExpandedDbItem(
dbItem: DbItem,
expandedDbItem: ExpandedDbItem,
) {
switch (dbItem.kind) {
case DbItemKind.RootLocal:
return expandedDbItem.kind === ExpandedDbItemKind.RootLocal;
case DbItemKind.LocalList:
return (
expandedDbItem.kind === ExpandedDbItemKind.LocalUserDefinedList &&
expandedDbItem.listName === dbItem.listName
);
case DbItemKind.RootRemote:
return expandedDbItem.kind === ExpandedDbItemKind.RootRemote;
case DbItemKind.RemoteUserDefinedList:
return (
expandedDbItem.kind === ExpandedDbItemKind.RemoteUserDefinedList &&
expandedDbItem.listName === dbItem.listName
);
default:
throw Error(`Unknown db item kind ${dbItem.kind}`);
}
}

View File

@@ -13,6 +13,7 @@ export enum DbItemKind {
export interface RootLocalDbItem {
kind: DbItemKind.RootLocal;
expanded: boolean;
children: LocalDbItem[];
}
@@ -20,6 +21,7 @@ export type LocalDbItem = LocalListDbItem | LocalDatabaseDbItem;
export interface LocalListDbItem {
kind: DbItemKind.LocalList;
expanded: boolean;
selected: boolean;
listName: string;
databases: LocalDatabaseDbItem[];
@@ -37,6 +39,7 @@ export interface LocalDatabaseDbItem {
export interface RootRemoteDbItem {
kind: DbItemKind.RootRemote;
expanded: boolean;
children: RemoteDbItem[];
}
@@ -62,6 +65,7 @@ export interface RemoteSystemDefinedListDbItem {
export interface RemoteUserDefinedListDbItem {
kind: DbItemKind.RemoteUserDefinedList;
expanded: boolean;
selected: boolean;
listName: string;
repos: RemoteRepoDbItem[];

View File

@@ -3,6 +3,7 @@ import { AppEvent, AppEventEmitter } from "../common/events";
import { ValueResult } from "../common/value-result";
import { DbConfigStore } from "./config/db-config-store";
import { DbItem } from "./db-item";
import { calculateNewExpandedState } from "./db-item-expansion";
import {
getSelectedDbItem,
mapDbItemToSelectedDbItem,
@@ -54,4 +55,22 @@ export class DbManager {
await this.dbConfigStore.setSelectedDbItem(selectedDbItem);
}
}
public async updateDbItemExpandedState(
dbItem: DbItem,
itemExpanded: boolean,
): Promise<void> {
const configResult = this.dbConfigStore.getConfig();
if (configResult.isFailure) {
throw Error("Cannot update expanded state if config is not loaded");
}
const newExpandedItems = calculateNewExpandedState(
configResult.value.expanded,
dbItem,
itemExpanded,
);
await this.dbConfigStore.updateExpandedState(newExpandedItems);
}
}

View File

@@ -9,6 +9,16 @@ import { DbPanel } from "./ui/db-panel";
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
export class DbModule extends DisposableObject {
public readonly dbManager: DbManager;
private readonly dbConfigStore: DbConfigStore;
constructor(app: App) {
super();
this.dbConfigStore = new DbConfigStore(app);
this.dbManager = new DbManager(app, this.dbConfigStore);
}
public async initialize(app: App): Promise<void> {
if (
app.mode !== AppMode.Development ||
@@ -23,15 +33,13 @@ export class DbModule extends DisposableObject {
void extLogger.log("Initializing database module");
const dbConfigStore = new DbConfigStore(app);
await dbConfigStore.initialize();
await this.dbConfigStore.initialize();
const dbManager = new DbManager(app, dbConfigStore);
const dbPanel = new DbPanel(dbManager);
const dbPanel = new DbPanel(this.dbManager);
await dbPanel.initialize();
this.push(dbPanel);
this.push(dbConfigStore);
this.push(this.dbConfigStore);
const dbSelectionDecorationProvider = new DbSelectionDecorationProvider();
@@ -40,7 +48,7 @@ export class DbModule extends DisposableObject {
}
export async function initializeDbModule(app: App): Promise<DbModule> {
const dbModule = new DbModule();
const dbModule = new DbModule(app);
await dbModule.initialize(app);
return dbModule;
}

View File

@@ -1,5 +1,6 @@
import {
DbConfig,
ExpandedDbItemKind,
LocalDatabase,
LocalList,
RemoteRepositoryList,
@@ -34,6 +35,10 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
createRepoItem(r, dbConfig),
);
const expanded =
dbConfig.expanded &&
dbConfig.expanded.some((e) => e.kind === ExpandedDbItemKind.RootRemote);
return {
kind: DbItemKind.RootRemote,
children: [
@@ -42,6 +47,7 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
...userDefinedRepoLists,
...repos,
],
expanded: !!expanded,
};
}
@@ -53,9 +59,14 @@ export function createLocalTree(dbConfig: DbConfig): RootLocalDbItem {
createLocalDb(l, dbConfig),
);
const expanded =
dbConfig.expanded &&
dbConfig.expanded.some((e) => e.kind === ExpandedDbItemKind.RootLocal);
return {
kind: DbItemKind.RootLocal,
children: [...localLists, ...localDbs],
expanded: !!expanded,
};
}
@@ -88,11 +99,20 @@ function createRemoteUserDefinedList(
dbConfig.selected.kind === SelectedDbItemKind.RemoteUserDefinedList &&
dbConfig.selected.listName === list.name;
const expanded =
dbConfig.expanded &&
dbConfig.expanded.some(
(e) =>
e.kind === ExpandedDbItemKind.RemoteUserDefinedList &&
e.listName === list.name,
);
return {
kind: DbItemKind.RemoteUserDefinedList,
listName: list.name,
repos: list.repositories.map((r) => createRepoItem(r, dbConfig, list.name)),
selected: !!selected,
expanded: !!expanded,
};
}
@@ -134,11 +154,20 @@ function createLocalList(list: LocalList, dbConfig: DbConfig): LocalListDbItem {
dbConfig.selected.kind === SelectedDbItemKind.LocalUserDefinedList &&
dbConfig.selected.listName === list.name;
const expanded =
dbConfig.expanded &&
dbConfig.expanded.some(
(e) =>
e.kind === ExpandedDbItemKind.LocalUserDefinedList &&
e.listName === list.name,
);
return {
kind: DbItemKind.LocalList,
listName: list.name,
databases: list.databases.map((d) => createLocalDb(d, dbConfig, list.name)),
selected: !!selected,
expanded: !!expanded,
};
}

View File

@@ -1,4 +1,4 @@
import { window, workspace } from "vscode";
import { TreeViewExpansionEvent, window, workspace } from "vscode";
import { commandRunner } from "../../commandRunner";
import { DisposableObject } from "../../pure/disposable-object";
import { DbManager } from "../db-manager";
@@ -18,6 +18,9 @@ export class DbPanel extends DisposableObject {
canSelectMany: false,
});
treeView.onDidCollapseElement.bind(this.onDidCollapseElement);
treeView.onDidExpandElement.bind(this.onDidExpandElement);
this.push(treeView);
}
@@ -49,4 +52,26 @@ export class DbPanel extends DisposableObject {
}
await this.dbManager.setSelectedDbItem(treeViewItem.dbItem);
}
private async onDidCollapseElement(
event: TreeViewExpansionEvent<DbTreeViewItem>,
): Promise<void> {
const dbItem = event.element.dbItem;
if (!dbItem) {
throw Error("Expected a database item.");
}
await this.dbManager.updateDbItemExpandedState(event.element.dbItem, false);
}
private async onDidExpandElement(
event: TreeViewExpansionEvent<DbTreeViewItem>,
): Promise<void> {
const dbItem = event.element.dbItem;
if (!dbItem) {
throw Error("Expected a database item.");
}
await this.dbManager.updateDbItemExpandedState(event.element.dbItem, true);
}
}

View File

@@ -70,7 +70,7 @@ export function createDbTreeViewItemRoot(
undefined,
label,
tooltip,
vscode.TreeItemCollapsibleState.Collapsed,
getCollapsibleState(dbItem.expanded),
children,
);
}
@@ -100,7 +100,7 @@ export function createDbTreeViewItemUserDefinedList(
undefined,
listName,
undefined,
vscode.TreeItemCollapsibleState.Collapsed,
getCollapsibleState(dbItem.expanded),
children,
);
}
@@ -147,3 +147,11 @@ export function createDbTreeViewItemLocalDatabase(
[],
);
}
function getCollapsibleState(
expanded: boolean,
): vscode.TreeItemCollapsibleState {
return expanded
? vscode.TreeItemCollapsibleState.Expanded
: vscode.TreeItemCollapsibleState.Collapsed;
}

View File

@@ -622,6 +622,11 @@ async function activateWithInstalledDistribution(
ctx.subscriptions.push(localQueryResultsView);
void extLogger.log("Initializing variant analysis manager.");
const app = new ExtensionApp(ctx);
const dbModule = await initializeDbModule(app);
ctx.subscriptions.push(dbModule);
const variantAnalysisStorageDir = join(
ctx.globalStorageUri.fsPath,
"variant-analyses",
@@ -636,6 +641,7 @@ async function activateWithInstalledDistribution(
cliServer,
variantAnalysisStorageDir,
variantAnalysisResultsManager,
dbModule.dbManager,
);
ctx.subscriptions.push(variantAnalysisManager);
ctx.subscriptions.push(variantAnalysisResultsManager);
@@ -1580,10 +1586,6 @@ async function activateWithInstalledDistribution(
void extLogger.log("Reading query history");
await qhm.readQueryHistory();
const app = new ExtensionApp(ctx);
const dbModule = await initializeDbModule(app);
ctx.subscriptions.push(dbModule);
void extLogger.log("Successfully finished extension initialization.");
return {

View File

@@ -4,9 +4,12 @@ import { extLogger } from "../common";
import {
getRemoteRepositoryLists,
getRemoteRepositoryListsPath,
isNewQueryRunExperienceEnabled,
} from "../config";
import { OWNER_REGEX, REPO_REGEX } from "../pure/helpers-pure";
import { UserCancellationException } from "../commandRunner";
import { DbManager } from "../databases/db-manager";
import { DbItemKind } from "../databases/db-item";
export interface RepositorySelection {
repositories?: string[];
@@ -30,7 +33,33 @@ interface RepoList {
* Gets the repositories or repository lists to run the query against.
* @returns The user selection.
*/
export async function getRepositorySelection(): Promise<RepositorySelection> {
export async function getRepositorySelection(
dbManager?: DbManager,
): Promise<RepositorySelection> {
if (isNewQueryRunExperienceEnabled()) {
const selectedDbItem = dbManager?.getSelectedDbItem();
if (selectedDbItem) {
switch (selectedDbItem.kind) {
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
throw new Error("Local databases and lists are not supported yet.");
case DbItemKind.RemoteSystemDefinedList:
return { repositoryLists: [selectedDbItem.listName] };
case DbItemKind.RemoteUserDefinedList:
return {
repositories: selectedDbItem.repos.map((repo) => repo.repoFullName),
};
case DbItemKind.RemoteOwner:
return { owners: [selectedDbItem.ownerName] };
case DbItemKind.RemoteRepo:
return { repositories: [selectedDbItem.repoFullName] };
}
} else {
throw new Error(
"Please select a remote database to run the query against.",
);
}
}
const quickPickItems = [
createCustomRepoQuickPickItem(),
createAllReposOfOwnerQuickPickItem(),
@@ -49,15 +78,21 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
options,
);
if (quickpick?.repositories?.length) {
if (!quickpick) {
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
// We set 'true' to make this a silent exception.
throw new UserCancellationException("No repositories selected", true);
}
if (quickpick.repositories?.length) {
void extLogger.log(
`Selected repositories: ${quickpick.repositories.join(", ")}`,
);
return { repositories: quickpick.repositories };
} else if (quickpick?.repositoryList) {
} else if (quickpick.repositoryList) {
void extLogger.log(`Selected repository list: ${quickpick.repositoryList}`);
return { repositoryLists: [quickpick.repositoryList] };
} else if (quickpick?.useCustomRepo) {
} else if (quickpick.useCustomRepo) {
const customRepo = await getCustomRepo();
if (customRepo === undefined) {
// The user cancelled, do nothing.
@@ -70,7 +105,7 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
}
void extLogger.log(`Entered repository: ${customRepo}`);
return { repositories: [customRepo] };
} else if (quickpick?.useAllReposOfOwner) {
} else if (quickpick.useAllReposOfOwner) {
const owner = await getOwner();
if (owner === undefined) {
// The user cancelled, do nothing.
@@ -82,9 +117,9 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
void extLogger.log(`Entered owner: ${owner}`);
return { owners: [owner] };
} else {
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
// We set 'true' to make this a silent exception.
throw new UserCancellationException("No repositories selected", true);
// This means the user has selected something, but there is nothing actually linked to this item. We want to show
// this to the user.
throw new UserCancellationException("No repositories selected", false);
}
}

View File

@@ -29,6 +29,7 @@ import {
RepositorySelection,
} from "./repository-selection";
import { Repository } from "./shared/repository";
import { DbManager } from "../databases/db-manager";
export interface QlPack {
name: string;
@@ -213,6 +214,7 @@ export async function prepareRemoteQueryRun(
uri: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
dbManager?: DbManager, // the dbManager is only needed when the newQueryRunExperience is enabled
): Promise<PreparedRemoteQuery> {
if (!(await cliServer.cliConstraints.supportsRemoteQueries())) {
throw new Error(
@@ -232,7 +234,7 @@ export async function prepareRemoteQueryRun(
message: "Determining query target language",
});
const repoSelection = await getRepositorySelection();
const repoSelection = await getRepositorySelection(dbManager);
if (!isValidSelection(repoSelection)) {
throw new UserCancellationException("No repositories to query.");
}

View File

@@ -59,6 +59,7 @@ import {
RepositoriesFilterSortStateWithIds,
} from "../pure/variant-analysis-filter-sort";
import { URLSearchParams } from "url";
import { DbManager } from "../databases/db-manager";
export class VariantAnalysisManager
extends DisposableObject
@@ -100,6 +101,7 @@ export class VariantAnalysisManager
private readonly cliServer: CodeQLCliServer,
private readonly storagePath: string,
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
private readonly dbManager: DbManager,
) {
super();
this.variantAnalysisMonitor = this.push(
@@ -140,6 +142,7 @@ export class VariantAnalysisManager
uri,
progress,
token,
this.dbManager,
);
const queryName = getQueryName(queryMetadata, queryFile);

View File

@@ -227,7 +227,7 @@ export const RepoRow = ({
[onSelectedChange, repository],
);
const disabled = !canExpand(status, downloadStatus);
const disabled = !canExpand(status, downloadStatus) || resultsLoading;
const expandableContentLoaded = isExpandableContentLoaded(
status,
downloadStatus,
@@ -247,11 +247,13 @@ export const RepoRow = ({
checked={selected}
disabled={!repository.id || !canSelect(status, downloadStatus)}
/>
{isExpanded ? (
{isExpanded && (
<ExpandCollapseCodicon name="chevron-down" label="Collapse" />
) : (
)}
{!isExpanded && !resultsLoading && (
<ExpandCollapseCodicon name="chevron-right" label="Expand" />
)}
{resultsLoading && <LoadingIcon label="Results are loading" />}
<VSCodeBadge>
{resultCount === undefined ? "-" : formatDecimal(resultCount)}
</VSCodeBadge>

View File

@@ -41,6 +41,7 @@ import {
VariantAnalysis,
VariantAnalysisScannedRepository,
VariantAnalysisScannedRepositoryDownloadStatus,
VariantAnalysisScannedRepositoryState,
VariantAnalysisStatus,
} from "../../../remote-queries/shared/variant-analysis";
import { createTimestampFile } from "../../../helpers";
@@ -56,6 +57,7 @@ import {
defaultFilterSortState,
SortKey,
} from "../../../pure/variant-analysis-filter-sort";
import { DbManager } from "../../../databases/db-manager";
// up to 3 minutes per test
jest.setTimeout(3 * 60 * 1000);
@@ -69,6 +71,7 @@ describe("Variant Analysis Manager", () => {
let cancellationTokenSource: CancellationTokenSource;
let variantAnalysisManager: VariantAnalysisManager;
let variantAnalysisResultsManager: VariantAnalysisResultsManager;
let dbManager: DbManager;
let variantAnalysis: VariantAnalysis;
let scannedRepos: VariantAnalysisScannedRepository[];
@@ -107,6 +110,7 @@ describe("Variant Analysis Manager", () => {
cli,
storagePath,
variantAnalysisResultsManager,
dbManager,
);
});
@@ -362,92 +366,94 @@ describe("Variant Analysis Manager", () => {
});
});
describe("when credentials are invalid", () => {
beforeEach(async () => {
jest
.spyOn(Credentials, "initialize")
.mockResolvedValue(undefined as unknown as Credentials);
});
it("should return early if credentials are wrong", async () => {
try {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
} catch (error: any) {
expect(error.message).toBe("Error authenticating with GitHub");
}
});
});
describe("when credentials are valid", () => {
let arrayBuffer: ArrayBuffer;
let getVariantAnalysisRepoStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepo
>;
let getVariantAnalysisRepoResultStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepoResult
>;
beforeEach(async () => {
const mockCredentials = {
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
const sourceFilePath = join(
__dirname,
"../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip",
);
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
getVariantAnalysisRepoStub = jest.spyOn(
ghApiClient,
"getVariantAnalysisRepo",
);
getVariantAnalysisRepoResultStub = jest.spyOn(
ghApiClient,
"getVariantAnalysisRepoResult",
);
});
describe("when the artifact_url is missing", () => {
describe("autoDownloadVariantAnalysisResult", () => {
describe("when credentials are invalid", () => {
beforeEach(async () => {
const dummyRepoTask = createMockVariantAnalysisRepoTask();
delete dummyRepoTask.artifact_url;
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
jest
.spyOn(Credentials, "initialize")
.mockResolvedValue(undefined as unknown as Credentials);
});
it("should not try to download the result", async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(getVariantAnalysisRepoResultStub).not.toHaveBeenCalled();
it("should return early if credentials are wrong", async () => {
try {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
} catch (error: any) {
expect(error.message).toBe("Error authenticating with GitHub");
}
});
});
describe("when the artifact_url is present", () => {
let dummyRepoTask: VariantAnalysisRepoTask;
describe("when credentials are valid", () => {
let arrayBuffer: ArrayBuffer;
let getVariantAnalysisRepoStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepo
>;
let getVariantAnalysisRepoResultStub: jest.SpiedFunction<
typeof ghApiClient.getVariantAnalysisRepoResult
>;
beforeEach(async () => {
dummyRepoTask = createMockVariantAnalysisRepoTask();
const mockCredentials = {
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest
.spyOn(Credentials, "initialize")
.mockResolvedValue(mockCredentials);
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
const sourceFilePath = join(
__dirname,
"../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip",
);
arrayBuffer = fs.readFileSync(sourceFilePath).buffer;
getVariantAnalysisRepoStub = jest.spyOn(
ghApiClient,
"getVariantAnalysisRepo",
);
getVariantAnalysisRepoResultStub = jest.spyOn(
ghApiClient,
"getVariantAnalysisRepoResult",
);
});
describe("autoDownloadVariantAnalysisResult", () => {
describe("when the artifact_url is missing", () => {
beforeEach(async () => {
const dummyRepoTask = createMockVariantAnalysisRepoTask();
delete dummyRepoTask.artifact_url;
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
});
it("should not try to download the result", async () => {
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
expect(getVariantAnalysisRepoResultStub).not.toHaveBeenCalled();
});
});
describe("when the artifact_url is present", () => {
let dummyRepoTask: VariantAnalysisRepoTask;
beforeEach(async () => {
dummyRepoTask = createMockVariantAnalysisRepoTask();
getVariantAnalysisRepoStub.mockResolvedValue(dummyRepoTask);
getVariantAnalysisRepoResultStub.mockResolvedValue(arrayBuffer);
});
it("should return early if variant analysis is cancelled", async () => {
cancellationTokenSource.cancel();
@@ -623,26 +629,18 @@ describe("Variant Analysis Manager", () => {
});
it("should update the repo state correctly", async () => {
// To set some initial repo states, we need to mock the correct methods so that the repo states are read in.
// The actual tests for these are in rehydrateVariantAnalysis, so we can just mock them here and test that
// the methods are called.
pathExistsStub.mockImplementation(() => true);
// This will read in the correct repo states
readJsonStub.mockImplementation(() =>
Promise.resolve({
[scannedRepos[1].repository.id]: {
repositoryId: scannedRepos[1].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
[scannedRepos[2].repository.id]: {
repositoryId: scannedRepos[2].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
},
}),
);
mockRepoStates({
[scannedRepos[1].repository.id]: {
repositoryId: scannedRepos[1].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
},
[scannedRepos[2].repository.id]: {
repositoryId: scannedRepos[2].repository.id,
downloadStatus:
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
},
});
await variantAnalysisManager.rehydrateVariantAnalysis(
variantAnalysis,
@@ -693,78 +691,91 @@ describe("Variant Analysis Manager", () => {
},
);
});
});
describe("enqueueDownload", () => {
it("should pop download tasks off the queue", async () => {
const getResultsSpy = jest.spyOn(
variantAnalysisManager,
"autoDownloadVariantAnalysisResult",
);
await variantAnalysisManager.enqueueDownload(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
await variantAnalysisManager.enqueueDownload(
scannedRepos[1],
variantAnalysis,
cancellationTokenSource.token,
);
await variantAnalysisManager.enqueueDownload(
scannedRepos[2],
variantAnalysis,
cancellationTokenSource.token,
);
expect(variantAnalysisManager.downloadsQueueSize()).toBe(0);
expect(getResultsSpy).toBeCalledTimes(3);
});
});
describe("removeVariantAnalysis", () => {
let removeAnalysisResultsStub: jest.SpiedFunction<
typeof variantAnalysisResultsManager.removeAnalysisResults
>;
let removeStorageStub: jest.SpiedFunction<typeof fs.remove>;
let dummyVariantAnalysis: VariantAnalysis;
beforeEach(async () => {
dummyVariantAnalysis = createMockVariantAnalysis({});
removeAnalysisResultsStub = jest
.spyOn(variantAnalysisResultsManager, "removeAnalysisResults")
.mockReturnValue(undefined);
removeStorageStub = jest
.spyOn(fs, "remove")
.mockReturnValue(undefined);
});
it("should remove variant analysis", async () => {
function mockRepoStates(
repoStates: Record<number, VariantAnalysisScannedRepositoryState>,
) {
pathExistsStub.mockImplementation(() => true);
await variantAnalysisManager.rehydrateVariantAnalysis(
dummyVariantAnalysis,
);
expect(pathExistsStub).toBeCalledWith(
join(storagePath, dummyVariantAnalysis.id.toString()),
);
expect(variantAnalysisManager.variantAnalysesSize).toBe(1);
await variantAnalysisManager.removeVariantAnalysis(
dummyVariantAnalysis,
);
expect(removeAnalysisResultsStub).toBeCalledTimes(1);
expect(removeStorageStub).toBeCalledTimes(1);
expect(variantAnalysisManager.variantAnalysesSize).toBe(0);
});
// This will read in the correct repo states
readJsonStub.mockImplementation(() => Promise.resolve(repoStates));
}
});
});
});
describe("when rehydrating a query", () => {
describe("enqueueDownload", () => {
beforeEach(async () => {
const mockCredentials = {
getOctokit: () =>
Promise.resolve({
request: jest.fn(),
}),
} as unknown as Credentials;
jest.spyOn(Credentials, "initialize").mockResolvedValue(mockCredentials);
});
it("should pop download tasks off the queue", async () => {
const getResultsSpy = jest
.spyOn(variantAnalysisManager, "autoDownloadVariantAnalysisResult")
.mockResolvedValue(undefined);
await variantAnalysisManager.enqueueDownload(
scannedRepos[0],
variantAnalysis,
cancellationTokenSource.token,
);
await variantAnalysisManager.enqueueDownload(
scannedRepos[1],
variantAnalysis,
cancellationTokenSource.token,
);
await variantAnalysisManager.enqueueDownload(
scannedRepos[2],
variantAnalysis,
cancellationTokenSource.token,
);
expect(variantAnalysisManager.downloadsQueueSize()).toBe(0);
expect(getResultsSpy).toBeCalledTimes(3);
});
});
describe("removeVariantAnalysis", () => {
let removeAnalysisResultsStub: jest.SpiedFunction<
typeof variantAnalysisResultsManager.removeAnalysisResults
>;
let removeStorageStub: jest.SpiedFunction<typeof fs.remove>;
let dummyVariantAnalysis: VariantAnalysis;
beforeEach(async () => {
dummyVariantAnalysis = createMockVariantAnalysis({});
removeAnalysisResultsStub = jest
.spyOn(variantAnalysisResultsManager, "removeAnalysisResults")
.mockReturnValue(undefined);
removeStorageStub = jest.spyOn(fs, "remove").mockReturnValue(undefined);
});
it("should remove variant analysis", async () => {
pathExistsStub.mockImplementation(() => true);
await variantAnalysisManager.rehydrateVariantAnalysis(
dummyVariantAnalysis,
);
expect(pathExistsStub).toBeCalledWith(
join(storagePath, dummyVariantAnalysis.id.toString()),
);
expect(variantAnalysisManager.variantAnalysesSize).toBe(1);
await variantAnalysisManager.removeVariantAnalysis(dummyVariantAnalysis);
expect(removeAnalysisResultsStub).toBeCalledTimes(1);
expect(removeStorageStub).toBeCalledTimes(1);
expect(variantAnalysisManager.variantAnalysesSize).toBe(0);
});
});
describe("rehydrateVariantAnalysis", () => {
let variantAnalysis: VariantAnalysis;
const variantAnalysisRemovedSpy = jest.fn();
let executeCommandSpy: jest.SpiedFunction<typeof commands.executeCommand>;

View File

@@ -257,6 +257,65 @@ describe("Variant Analysis Monitor", () => {
});
});
describe("when the responses change", () => {
let scannedRepos: ApiVariantAnalysisScannedRepository[];
beforeEach(async () => {
scannedRepos = createMockScannedRepos([
"pending",
"in_progress",
"in_progress",
"in_progress",
"pending",
"pending",
]);
mockApiResponse = createMockApiResponse("in_progress", scannedRepos);
mockGetVariantAnalysis.mockResolvedValueOnce(mockApiResponse);
let nextApiResponse = {
...mockApiResponse,
scanned_repositories: [...scannedRepos.map((r) => ({ ...r }))],
};
nextApiResponse.scanned_repositories[0].analysis_status = "succeeded";
nextApiResponse.scanned_repositories[1].analysis_status = "succeeded";
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
nextApiResponse = {
...mockApiResponse,
scanned_repositories: [
...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
],
};
nextApiResponse.scanned_repositories[2].analysis_status = "succeeded";
nextApiResponse.scanned_repositories[5].analysis_status = "succeeded";
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
nextApiResponse = {
...mockApiResponse,
scanned_repositories: [
...nextApiResponse.scanned_repositories.map((r) => ({ ...r })),
],
};
nextApiResponse.scanned_repositories[3].analysis_status = "succeeded";
nextApiResponse.scanned_repositories[4].analysis_status = "failed";
mockGetVariantAnalysis.mockResolvedValueOnce(nextApiResponse);
});
it("should trigger a download extension command for each repo", async () => {
const commandSpy = jest
.spyOn(commands, "executeCommand")
.mockResolvedValue(undefined);
await variantAnalysisMonitor.monitorVariantAnalysis(
variantAnalysis,
cancellationTokenSource.token,
);
expect(mockGetVariantAnalysis).toBeCalledTimes(4);
expect(commandSpy).toBeCalledTimes(5);
});
});
describe("when there are no repos to scan", () => {
beforeEach(async () => {
scannedRepos = [];

View File

@@ -60,6 +60,7 @@ describe("db panel", () => {
databases: [],
},
},
expanded: [],
};
await saveDbConfig(dbConfig);
@@ -123,6 +124,7 @@ describe("db panel", () => {
databases: [],
},
},
expanded: [],
};
await saveDbConfig(dbConfig);
@@ -174,6 +176,7 @@ describe("db panel", () => {
databases: [],
},
},
expanded: [],
};
await saveDbConfig(dbConfig);
@@ -213,6 +216,7 @@ describe("db panel", () => {
databases: [],
},
},
expanded: [],
};
await saveDbConfig(dbConfig);
@@ -281,6 +285,7 @@ describe("db panel", () => {
databases: [],
},
},
expanded: [],
};
await saveDbConfig(dbConfig);
@@ -359,6 +364,7 @@ describe("db panel", () => {
],
},
},
expanded: [],
};
await saveDbConfig(dbConfig);
@@ -421,6 +427,7 @@ describe("db panel", () => {
databases: [],
},
},
expanded: [],
selected: {
kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: "my-list-2",
@@ -477,6 +484,7 @@ describe("db panel", () => {
databases: [],
},
},
expanded: [],
selected: {
kind: SelectedDbItemKind.RemoteRepository,
repositoryName: "owner1/repo1",

View File

@@ -4,169 +4,152 @@ import { UserCancellationException } from "../../../commandRunner";
import * as config from "../../../config";
import { getRepositorySelection } from "../../../remote-queries/repository-selection";
import { DbManager } from "../../../databases/db-manager";
import { DbItem, DbItemKind } from "../../../databases/db-item";
describe("repository selection", () => {
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
describe("newQueryRunExperience true", () => {
beforeEach(() => {
jest
.spyOn(config, "isNewQueryRunExperienceEnabled")
.mockReturnValue(true);
});
let getRemoteRepositoryListsSpy: jest.SpiedFunction<
typeof config.getRemoteRepositoryLists
>;
let getRemoteRepositoryListsPathSpy: jest.SpiedFunction<
typeof config.getRemoteRepositoryListsPath
>;
it("should throw error when no database item is selected", async () => {
const dbManager = setUpDbManager(undefined);
let pathExistsStub: jest.SpiedFunction<typeof fs.pathExists>;
let fsStatStub: jest.SpiedFunction<typeof fs.stat>;
let fsReadFileStub: jest.SpiedFunction<typeof fs.readFile>;
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
Error("Please select a remote database to run the query against."),
);
});
beforeEach(() => {
quickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValue(undefined);
showInputBoxSpy = jest
.spyOn(window, "showInputBox")
.mockResolvedValue(undefined);
it("should throw error when local database item is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.LocalDatabase,
} as DbItem);
getRemoteRepositoryListsSpy = jest
.spyOn(config, "getRemoteRepositoryLists")
.mockReturnValue(undefined);
getRemoteRepositoryListsPathSpy = jest
.spyOn(config, "getRemoteRepositoryListsPath")
.mockReturnValue(undefined);
await expect(getRepositorySelection(dbManager)).rejects.toThrow(
Error("Local databases and lists are not supported yet."),
);
});
pathExistsStub = jest
.spyOn(fs, "pathExists")
.mockImplementation(() => false);
fsStatStub = jest
.spyOn(fs, "stat")
.mockRejectedValue(new Error("not found"));
fsReadFileStub = jest
.spyOn(fs, "readFile")
.mockRejectedValue(new Error("not found"));
});
it("should return correct selection when remote system defined list is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteSystemDefinedList,
listName: "top_10",
} as DbItem);
describe("repo lists from settings", () => {
it("should allow selection from repo lists from your pre-defined config", async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
repositories: ["foo/bar", "foo/baz"],
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({
list1: ["foo/bar", "foo/baz"],
list2: [],
});
const repoSelection = await getRepositorySelection(dbManager);
// Make the function call
const repoSelection = await getRepositorySelection();
expect(repoSelection.repositoryLists).toEqual(["top_10"]);
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toBeUndefined();
});
it("should return correct selection when remote user defined list is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteUserDefinedList,
repos: [
{ repoFullName: "owner1/repo1" },
{ repoFullName: "owner1/repo2" },
],
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
// Check that the return value is correct
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual(["foo/bar", "foo/baz"]);
expect(repoSelection.repositories).toEqual([
"owner1/repo1",
"owner1/repo2",
]);
});
});
describe("system level repo lists", () => {
it("should allow selection from repo lists defined at the system level", async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
repositoryList: "top_100",
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({
list1: ["foo/bar", "foo/baz"],
list2: [],
});
it("should return correct selection when remote owner is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteOwner,
ownerName: "owner2",
} as DbItem);
// Make the function call
const repoSelection = await getRepositorySelection();
const repoSelection = await getRepositorySelection(dbManager);
// Check that the return value is correct
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toEqual(["owner2"]);
expect(repoSelection.repositories).toBeUndefined();
});
it("should return correct selection when remote repo is selected", async () => {
const dbManager = setUpDbManager({
kind: DbItemKind.RemoteRepo,
repoFullName: "owner1/repo2",
} as DbItem);
const repoSelection = await getRepositorySelection(dbManager);
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositoryLists).toEqual(["top_100"]);
expect(repoSelection.repositories).toEqual(["owner1/repo2"]);
});
function setUpDbManager(response: DbItem | undefined): DbManager {
return {
getSelectedDbItem: jest.fn(() => {
return response;
}),
} as any as DbManager;
}
});
describe("custom owner", () => {
// Test the owner regex in various "good" cases
const goodOwners = [
"owner",
"owner-with-hyphens",
"ownerWithNumbers58",
"owner_with_underscores",
"owner.with.periods.",
];
goodOwners.forEach((owner) => {
it(`should run on a valid owner that you enter in the text box: ${owner}`, async () => {
describe("newQueryRunExperience false", () => {
let quickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
let getRemoteRepositoryListsSpy: jest.SpiedFunction<
typeof config.getRemoteRepositoryLists
>;
let getRemoteRepositoryListsPathSpy: jest.SpiedFunction<
typeof config.getRemoteRepositoryListsPath
>;
let pathExistsStub: jest.SpiedFunction<typeof fs.pathExists>;
let fsStatStub: jest.SpiedFunction<typeof fs.stat>;
let fsReadFileStub: jest.SpiedFunction<typeof fs.readFile>;
beforeEach(() => {
quickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValue(undefined);
showInputBoxSpy = jest
.spyOn(window, "showInputBox")
.mockResolvedValue(undefined);
getRemoteRepositoryListsSpy = jest
.spyOn(config, "getRemoteRepositoryLists")
.mockReturnValue(undefined);
getRemoteRepositoryListsPathSpy = jest
.spyOn(config, "getRemoteRepositoryListsPath")
.mockReturnValue(undefined);
pathExistsStub = jest
.spyOn(fs, "pathExists")
.mockImplementation(() => false);
fsStatStub = jest
.spyOn(fs, "stat")
.mockRejectedValue(new Error("not found"));
fsReadFileStub = jest
.spyOn(fs, "readFile")
.mockRejectedValue(new Error("not found"));
});
describe("repo lists from settings", () => {
it("should allow selection from repo lists from your pre-defined config", async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
repositories: ["foo/bar", "foo/baz"],
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(owner);
// Make the function call
const repoSelection = await getRepositorySelection();
// Check that the return value is correct
expect(repoSelection.repositories).toBeUndefined();
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toEqual([owner]);
});
});
// Test the owner regex in various "bad" cases
const badOwners = ["invalid&owner", "owner-with-repo/repo"];
badOwners.forEach((owner) => {
it(`should show an error message if you enter an invalid owner in the text box: ${owner}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(owner);
// Function call should throw a UserCancellationException
await expect(getRepositorySelection()).rejects.toThrow(
`Invalid user or organization: ${owner}`,
);
});
});
it("should be ok for the user to change their mind", async () => {
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({});
// The user pressed escape to cancel the operation
showInputBoxSpy.mockResolvedValue(undefined);
await expect(getRepositorySelection()).rejects.toThrow(
"No repositories selected",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
});
});
describe("custom repo", () => {
// Test the repo regex in various "good" cases
const goodRepos = [
"owner/repo",
"owner_with.symbols-/repo.with-symbols_",
"ownerWithNumbers58/repoWithNumbers37",
];
goodRepos.forEach((repo) => {
it(`should run on a valid repo that you enter in the text box: ${repo}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(repo);
getRemoteRepositoryListsSpy.mockReturnValue({
list1: ["foo/bar", "foo/baz"],
list2: [],
});
// Make the function call
const repoSelection = await getRepositorySelection();
@@ -174,44 +157,290 @@ describe("repository selection", () => {
// Check that the return value is correct
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual([repo]);
expect(repoSelection.repositories).toEqual(["foo/bar", "foo/baz"]);
});
});
// Test the repo regex in various "bad" cases
const badRepos = [
"invalid*owner/repo",
"owner/repo+some&invalid&stuff",
"owner-with-no-repo/",
"/repo-with-no-owner",
];
badRepos.forEach((repo) => {
it(`should show an error message if you enter an invalid repo in the text box: ${repo}`, async () => {
it("should return an error for an empty repository list", async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
repositories: [],
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(repo);
getRemoteRepositoryListsSpy.mockReturnValue({
list1: ["foo/bar", "foo/baz"],
list2: [],
});
// Function call should throw a UserCancellationException
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository format",
"No repositories selected",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
await expect(getRepositorySelection()).rejects.toHaveProperty(
"silent",
false,
);
});
});
it("should be ok for the user to change their mind", async () => {
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({});
describe("system level repo lists", () => {
it("should allow selection from repo lists defined at the system level", async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
repositoryList: "top_100",
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({
list1: ["foo/bar", "foo/baz"],
list2: [],
});
// The user pressed escape to cancel the operation
showInputBoxSpy.mockResolvedValue(undefined);
// Make the function call
const repoSelection = await getRepositorySelection();
// Check that the return value is correct
expect(repoSelection.repositories).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositoryLists).toEqual(["top_100"]);
});
});
describe("custom owner", () => {
// Test the owner regex in various "good" cases
const goodOwners = [
"owner",
"owner-with-hyphens",
"ownerWithNumbers58",
"owner_with_underscores",
"owner.with.periods.",
];
goodOwners.forEach((owner) => {
it(`should run on a valid owner that you enter in the text box: ${owner}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(owner);
// Make the function call
const repoSelection = await getRepositorySelection();
// Check that the return value is correct
expect(repoSelection.repositories).toBeUndefined();
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toEqual([owner]);
});
});
// Test the owner regex in various "bad" cases
const badOwners = ["invalid&owner", "owner-with-repo/repo"];
badOwners.forEach((owner) => {
it(`should show an error message if you enter an invalid owner in the text box: ${owner}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(owner);
// Function call should throw a UserCancellationException
await expect(getRepositorySelection()).rejects.toThrow(
`Invalid user or organization: ${owner}`,
);
});
});
it("should be ok for the user to change their mind", async () => {
quickPickSpy.mockResolvedValue({
useAllReposOfOwner: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({});
// The user pressed escape to cancel the operation
showInputBoxSpy.mockResolvedValue(undefined);
await expect(getRepositorySelection()).rejects.toThrow(
"No repositories selected",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
await expect(getRepositorySelection()).rejects.toHaveProperty(
"silent",
true,
);
});
});
describe("custom repo", () => {
// Test the repo regex in various "good" cases
const goodRepos = [
"owner/repo",
"owner_with.symbols-/repo.with-symbols_",
"ownerWithNumbers58/repoWithNumbers37",
];
goodRepos.forEach((repo) => {
it(`should run on a valid repo that you enter in the text box: ${repo}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(repo);
// Make the function call
const repoSelection = await getRepositorySelection();
// Check that the return value is correct
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual([repo]);
});
});
// Test the repo regex in various "bad" cases
const badRepos = [
"invalid*owner/repo",
"owner/repo+some&invalid&stuff",
"owner-with-no-repo/",
"/repo-with-no-owner",
];
badRepos.forEach((repo) => {
it(`should show an error message if you enter an invalid repo in the text box: ${repo}`, async () => {
// Fake return values
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({}); // no pre-defined repo lists
showInputBoxSpy.mockResolvedValue(repo);
// Function call should throw a UserCancellationException
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository format",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
});
});
it("should be ok for the user to change their mind", async () => {
quickPickSpy.mockResolvedValue({
useCustomRepo: true,
} as unknown as QuickPickItem);
getRemoteRepositoryListsSpy.mockReturnValue({});
// The user pressed escape to cancel the operation
showInputBoxSpy.mockResolvedValue(undefined);
await expect(getRepositorySelection()).rejects.toThrow(
"No repositories selected",
);
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
await expect(getRepositorySelection()).rejects.toHaveProperty(
"silent",
true,
);
});
});
describe("external repository lists file", () => {
it("should fail if path does not exist", async () => {
const fakeFilePath = "/path/that/does/not/exist.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => false);
await expect(getRepositorySelection()).rejects.toThrow(
`External repository lists file does not exist at ${fakeFilePath}`,
);
});
it("should fail if path points to directory", async () => {
const fakeFilePath = "/path/to/dir";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => true } as any);
await expect(getRepositorySelection()).rejects.toThrow(
"External repository lists path should not point to a directory",
);
});
it("should fail if file does not have valid JSON", async () => {
const fakeFilePath = "/path/to/file.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
fsReadFileStub.mockResolvedValue("not-json" as any as Buffer);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should contain valid JSON.",
);
});
it("should fail if file contains array", async () => {
const fakeFilePath = "/path/to/file.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
fsReadFileStub.mockResolvedValue("[]" as any as Buffer);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should be an object mapping names to a list of repositories.",
);
});
it("should fail if file does not contain repo lists in the right format", async () => {
const fakeFilePath = "/path/to/file.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
const repoLists = {
list1: "owner1/repo1",
};
fsReadFileStub.mockResolvedValue(
JSON.stringify(repoLists) as any as Buffer,
);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should contain an array of repositories for each list.",
);
});
it("should get repo lists from file", async () => {
const fakeFilePath = "/path/to/file.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
const repoLists = {
list1: ["owner1/repo1", "owner2/repo2"],
list2: ["owner3/repo3"],
};
fsReadFileStub.mockResolvedValue(
JSON.stringify(repoLists) as any as Buffer,
);
getRemoteRepositoryListsSpy.mockReturnValue({
list3: ["onwer4/repo4"],
list4: [],
});
quickPickSpy.mockResolvedValue({
repositories: ["owner3/repo3"],
} as unknown as QuickPickItem);
const repoSelection = await getRepositorySelection();
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual(["owner3/repo3"]);
});
});
it("should allow the user to cancel", async () => {
// Fake return values
quickPickSpy.mockResolvedValue(undefined);
await expect(getRepositorySelection()).rejects.toThrow(
"No repositories selected",
@@ -219,98 +448,10 @@ describe("repository selection", () => {
await expect(getRepositorySelection()).rejects.toThrow(
UserCancellationException,
);
});
});
describe("external repository lists file", () => {
it("should fail if path does not exist", async () => {
const fakeFilePath = "/path/that/does/not/exist.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => false);
await expect(getRepositorySelection()).rejects.toThrow(
`External repository lists file does not exist at ${fakeFilePath}`,
await expect(getRepositorySelection()).rejects.toHaveProperty(
"silent",
true,
);
});
it("should fail if path points to directory", async () => {
const fakeFilePath = "/path/to/dir";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => true } as any);
await expect(getRepositorySelection()).rejects.toThrow(
"External repository lists path should not point to a directory",
);
});
it("should fail if file does not have valid JSON", async () => {
const fakeFilePath = "/path/to/file.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
fsReadFileStub.mockResolvedValue("not-json" as any as Buffer);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should contain valid JSON.",
);
});
it("should fail if file contains array", async () => {
const fakeFilePath = "/path/to/file.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
fsReadFileStub.mockResolvedValue("[]" as any as Buffer);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should be an object mapping names to a list of repositories.",
);
});
it("should fail if file does not contain repo lists in the right format", async () => {
const fakeFilePath = "/path/to/file.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
const repoLists = {
list1: "owner1/repo1",
};
fsReadFileStub.mockResolvedValue(
JSON.stringify(repoLists) as any as Buffer,
);
await expect(getRepositorySelection()).rejects.toThrow(
"Invalid repository lists file. It should contain an array of repositories for each list.",
);
});
it("should get repo lists from file", async () => {
const fakeFilePath = "/path/to/file.json";
getRemoteRepositoryListsPathSpy.mockReturnValue(fakeFilePath);
pathExistsStub.mockImplementation(() => true);
fsStatStub.mockResolvedValue({ isDirectory: () => false } as any);
const repoLists = {
list1: ["owner1/repo1", "owner2/repo2"],
list2: ["owner3/repo3"],
};
fsReadFileStub.mockResolvedValue(
JSON.stringify(repoLists) as any as Buffer,
);
getRemoteRepositoryListsSpy.mockReturnValue({
list3: ["onwer4/repo4"],
list4: [],
});
quickPickSpy.mockResolvedValue({
repositories: ["owner3/repo3"],
} as unknown as QuickPickItem);
const repoSelection = await getRepositorySelection();
expect(repoSelection.repositoryLists).toBeUndefined();
expect(repoSelection.owners).toBeUndefined();
expect(repoSelection.repositories).toEqual(["owner3/repo3"]);
});
});
});

View File

@@ -0,0 +1,34 @@
import {
DbItemKind,
RemoteUserDefinedListDbItem,
RootRemoteDbItem,
} from "../../src/databases/db-item";
export function createRootRemoteDbItem(): RootRemoteDbItem {
return {
kind: DbItemKind.RootRemote,
children: [],
expanded: false,
};
}
export function createRemoteUserDefinedListDbItem({
name = "list1",
}: {
name: string;
}): RemoteUserDefinedListDbItem {
return {
kind: DbItemKind.RemoteUserDefinedList,
selected: false,
expanded: false,
listName: name,
repos: [
{
kind: DbItemKind.RemoteRepo,
selected: false,
repoFullName: "repo1",
parentListName: name,
},
],
};
}

View File

@@ -9,5 +9,6 @@
"lists": [],
"databases": []
}
}
},
"expanded": []
}

View File

@@ -45,6 +45,7 @@
]
}
},
"expanded": [],
"selected": {
"kind": "remoteUserDefinedList",
"listName": "repoList1"

View File

@@ -22,6 +22,7 @@ describe("db config validation", () => {
somethingElse: "bar",
},
},
expanded: [],
} as any as DbConfig;
const validationOutput = configValidator.validate(dbConfig);

View File

@@ -0,0 +1,109 @@
import {
ExpandedDbItem,
ExpandedDbItemKind,
} from "../../../src/databases/config/db-config";
import {
RemoteUserDefinedListDbItem,
RootRemoteDbItem,
} from "../../../src/databases/db-item";
import { calculateNewExpandedState } from "../../../src/databases/db-item-expansion";
import {
createRemoteUserDefinedListDbItem,
createRootRemoteDbItem,
} from "../../factories/db-item-factories";
describe("db item expansion", () => {
it("should add an expanded item to an existing list", () => {
const currentExpandedItems: ExpandedDbItem[] = [
{
kind: ExpandedDbItemKind.RootRemote,
},
{
kind: ExpandedDbItemKind.RemoteUserDefinedList,
listName: "list1",
},
];
const dbItem: RemoteUserDefinedListDbItem =
createRemoteUserDefinedListDbItem({
name: "list2",
});
const newExpandedItems = calculateNewExpandedState(
currentExpandedItems,
dbItem,
true,
);
expect(newExpandedItems).toEqual([
...currentExpandedItems,
{
kind: ExpandedDbItemKind.RemoteUserDefinedList,
listName: "list2",
},
]);
});
it("should add an expanded item to an empty list", () => {
const dbItem: RemoteUserDefinedListDbItem =
createRemoteUserDefinedListDbItem({
name: "list2",
});
const newExpandedItems = calculateNewExpandedState([], dbItem, true);
expect(newExpandedItems).toEqual([
{
kind: ExpandedDbItemKind.RemoteUserDefinedList,
listName: "list2",
},
]);
});
it("should remove a collapsed item from a list", () => {
const currentExpandedItems: ExpandedDbItem[] = [
{
kind: ExpandedDbItemKind.RootRemote,
},
{
kind: ExpandedDbItemKind.RemoteUserDefinedList,
listName: "list1",
},
];
const dbItem: RemoteUserDefinedListDbItem =
createRemoteUserDefinedListDbItem({
name: "list1",
});
const newExpandedItems = calculateNewExpandedState(
currentExpandedItems,
dbItem,
false,
);
expect(newExpandedItems).toEqual([
{
kind: ExpandedDbItemKind.RootRemote,
},
]);
});
it("should remove a collapsed item from a list that becomes empty", () => {
const currentExpandedItems: ExpandedDbItem[] = [
{
kind: ExpandedDbItemKind.RootRemote,
},
];
const dbItem: RootRemoteDbItem = createRootRemoteDbItem();
const newExpandedItems = calculateNewExpandedState(
currentExpandedItems,
dbItem,
false,
);
expect(newExpandedItems).toEqual([]);
});
});

View File

@@ -1,10 +1,12 @@
import { DbItem, DbItemKind } from "../../../src/databases/db-item";
import { getSelectedDbItem } from "../../../src/databases/db-item-selection";
describe("db item selection", () => {
it("should return undefined if no item is selected", () => {
const dbItems: DbItem[] = [
{
kind: DbItemKind.RootRemote,
expanded: false,
children: [
{
kind: DbItemKind.RemoteSystemDefinedList,
@@ -27,6 +29,7 @@ describe("db item selection", () => {
},
{
kind: DbItemKind.RemoteUserDefinedList,
expanded: false,
listName: "my list",
repos: [
{
@@ -46,9 +49,11 @@ describe("db item selection", () => {
},
{
kind: DbItemKind.RootLocal,
expanded: false,
children: [
{
kind: DbItemKind.LocalList,
expanded: false,
listName: "list-1",
databases: [
{
@@ -89,9 +94,11 @@ describe("db item selection", () => {
const dbItems: DbItem[] = [
{
kind: DbItemKind.RootLocal,
expanded: false,
children: [
{
kind: DbItemKind.LocalList,
expanded: false,
listName: "list-1",
databases: [
{
@@ -139,6 +146,7 @@ describe("db item selection", () => {
const dbItems: DbItem[] = [
{
kind: DbItemKind.RootRemote,
expanded: false,
children: [
{
kind: DbItemKind.RemoteSystemDefinedList,
@@ -154,6 +162,7 @@ describe("db item selection", () => {
},
{
kind: DbItemKind.RemoteUserDefinedList,
expanded: false,
listName: "my list",
repos: [
{
@@ -175,6 +184,7 @@ describe("db item selection", () => {
expect(getSelectedDbItem(dbItems)).toEqual({
kind: DbItemKind.RemoteUserDefinedList,
expanded: false,
listName: "my list",
repos: [
{
@@ -196,6 +206,7 @@ describe("db item selection", () => {
const dbItems: DbItem[] = [
{
kind: DbItemKind.RootRemote,
expanded: false,
children: [
{
kind: DbItemKind.RemoteSystemDefinedList,
@@ -211,6 +222,7 @@ describe("db item selection", () => {
},
{
kind: DbItemKind.RemoteUserDefinedList,
expanded: false,
listName: "my list",
repos: [
{
@@ -250,6 +262,7 @@ describe("db item selection", () => {
const dbItems: DbItem[] = [
{
kind: DbItemKind.RootRemote,
expanded: false,
children: [
{
kind: DbItemKind.RemoteSystemDefinedList,
@@ -265,6 +278,7 @@ describe("db item selection", () => {
},
{
kind: DbItemKind.RemoteUserDefinedList,
expanded: false,
listName: "my list",
repos: [],
selected: false,

View File

@@ -1,5 +1,6 @@
import {
DbConfig,
ExpandedDbItemKind,
SelectedDbItemKind,
} from "../../../src/databases/config/db-config";
import {
@@ -28,12 +29,14 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
};
const dbTreeRoot = createRemoteTree(dbConfig);
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
expect(dbTreeRoot.expanded).toBe(false);
expect(dbTreeRoot.children.length).toBe(3);
expect(dbTreeRoot.children[0]).toEqual({
kind: DbItemKind.RemoteSystemDefinedList,
@@ -80,6 +83,7 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
};
const dbTreeRoot = createRemoteTree(dbConfig);
@@ -94,6 +98,7 @@ describe("db tree creator", () => {
expect(repositoryListNodes[0]).toEqual({
kind: DbItemKind.RemoteUserDefinedList,
selected: false,
expanded: false,
listName: dbConfig.databases.remote.repositoryLists[0].name,
repos: dbConfig.databases.remote.repositoryLists[0].repositories.map(
(repo) => ({
@@ -107,6 +112,7 @@ describe("db tree creator", () => {
expect(repositoryListNodes[1]).toEqual({
kind: DbItemKind.RemoteUserDefinedList,
selected: false,
expanded: false,
listName: dbConfig.databases.remote.repositoryLists[1].name,
repos: dbConfig.databases.remote.repositoryLists[1].repositories.map(
(repo) => ({
@@ -132,6 +138,7 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
};
const dbTreeRoot = createRemoteTree(dbConfig);
@@ -166,6 +173,7 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
};
const dbTreeRoot = createRemoteTree(dbConfig);
@@ -215,6 +223,7 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
selected: {
kind: SelectedDbItemKind.RemoteUserDefinedList,
listName: "my-list-1",
@@ -246,6 +255,7 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
selected: {
kind: SelectedDbItemKind.RemoteOwner,
ownerName: "owner1",
@@ -278,6 +288,7 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
selected: {
kind: SelectedDbItemKind.RemoteRepository,
repositoryName: "owner1/repo2",
@@ -313,6 +324,7 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
selected: {
kind: SelectedDbItemKind.RemoteRepository,
listName: "my-list-1",
@@ -335,6 +347,81 @@ describe("db tree creator", () => {
expect(listNodes[0].repos[0].selected).toBe(true);
});
});
describe("expanded db items", () => {
it("should allow expanding the root remote list node", () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [],
owners: [],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
expanded: [
{
kind: ExpandedDbItemKind.RootRemote,
},
],
};
const dbTreeRoot = createRemoteTree(dbConfig);
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
expect(dbTreeRoot.expanded).toBe(true);
});
it("should allow expanding a remote user defined list node", () => {
const dbConfig: DbConfig = {
databases: {
remote: {
repositoryLists: [
{
name: "my-list-1",
repositories: [
"owner1/repo1",
"owner1/repo2",
"owner2/repo1",
],
},
],
owners: [],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
expanded: [
{
kind: ExpandedDbItemKind.RootRemote,
},
{
kind: ExpandedDbItemKind.RemoteUserDefinedList,
listName: "my-list-1",
},
],
};
const dbTreeRoot = createRemoteTree(dbConfig);
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootRemote);
expect(dbTreeRoot.expanded).toBe(true);
const repositoryListNodes = dbTreeRoot.children.filter(
isRemoteUserDefinedListDbItem,
);
expect(repositoryListNodes.length).toBe(1);
expect(repositoryListNodes[0].expanded).toEqual(true);
});
});
});
describe("createLocalTree", () => {
@@ -351,12 +438,14 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
};
const dbTreeRoot = createLocalTree(dbConfig);
expect(dbTreeRoot).toBeTruthy();
expect(dbTreeRoot.kind).toBe(DbItemKind.RootLocal);
expect(dbTreeRoot.expanded).toBe(false);
expect(dbTreeRoot.children.length).toBe(0);
});
@@ -402,6 +491,7 @@ describe("db tree creator", () => {
databases: [],
},
},
expanded: [],
};
const dbTreeRoot = createLocalTree(dbConfig);
@@ -415,6 +505,7 @@ describe("db tree creator", () => {
expect(localListNodes.length).toBe(2);
expect(localListNodes[0]).toEqual({
kind: DbItemKind.LocalList,
expanded: false,
selected: false,
listName: dbConfig.databases.local.lists[0].name,
databases: dbConfig.databases.local.lists[0].databases.map((db) => ({
@@ -429,6 +520,7 @@ describe("db tree creator", () => {
});
expect(localListNodes[1]).toEqual({
kind: DbItemKind.LocalList,
expanded: false,
selected: false,
listName: dbConfig.databases.local.lists[1].name,
databases: dbConfig.databases.local.lists[1].databases.map((db) => ({
@@ -469,6 +561,7 @@ describe("db tree creator", () => {
],
},
},
expanded: [],
};
const dbTreeRoot = createLocalTree(dbConfig);

View File

@@ -121,6 +121,60 @@
"required": ["remote", "local"],
"additionalProperties": false
},
"expanded": {
"type": "array",
"items": {
"type": "object",
"oneOf": [
{
"properties": {
"kind": {
"type": "string",
"enum": ["rootLocal"]
}
},
"required": ["kind"],
"additionalProperties": false
},
{
"properties": {
"kind": {
"type": "string",
"enum": ["localUserDefinedList"]
},
"listName": {
"type": "string"
}
},
"required": ["kind", "listName"],
"additionalProperties": false
},
{
"properties": {
"kind": {
"type": "string",
"enum": ["rootRemote"]
}
},
"required": ["kind"],
"additionalProperties": false
},
{
"properties": {
"kind": {
"type": "string",
"enum": ["remoteUserDefinedList"]
},
"listName": {
"type": "string"
}
},
"required": ["kind", "listName"],
"additionalProperties": false
}
]
}
},
"selected": {
"type": "object",
"oneOf": [
@@ -211,6 +265,6 @@
]
}
},
"required": ["databases"],
"required": ["databases", "expanded"],
"additionalProperties": false
}