cosmos-explorer/src/Explorer/Tree/treeNodeUtil.test.ts
Laurent Nguyen bcd8b7229f
Upgrade typescript to 4.9.5 and jest to 29.7.0 (and related packages) (#1884)
* Upgrade typescript to 4.9.5

* Fix compile issue and put back files in tsconfig.strict.json

* Update test snapshots

* Fix jest tests by upgrading jest and other related packages.

* Attempt to fix playwright test

* Revert "Attempt to fix playwright test"

This reverts commit 8293f34c9ce3901eb160a56526c59e3d3e1e343e.

* 2nd attempt to fix example test

* fix waitFor in playwright

* Remove unused describe section

* Attempt to fix e2e test

* Revert "Attempt to fix e2e test"

This reverts commit 9745bcd2efccf92e33e62e4563349cb76e6236de.

* Upgrade playwright to latest

* Revert "Upgrade playwright to latest"

This reverts commit e2ea1d01895f9db157a92a5167a7865b7cc43266.

* Error test on e2e

* Revert "Error test on e2e"

This reverts commit 124e3764f72fff70df9cc1a2e93d12faa7b507e9.

* Try to select dropdown item by xpath selector

* Revert "Try to select dropdown item by xpath selector"

This reverts commit 8eb42a64e23cee589a7c0a809c039ecc7867b7ba.

* Attempt to wait until page is fully loaded

* Revert "Attempt to wait until page is fully loaded"

This reverts commit bb43fcea6e71e593f7d1a3e374e9f1d57052ab70.

* Use playwright selectOption to select dropdown option

* Revert "Use playwright selectOption to select dropdown option"

This reverts commit daa8cd09304f68093df2d94f4bd1139cb367465a.

* Select dropdown option with playwright api instead of manual click

* c7ab4c7ecf7b05f32a85568bce1a667ad8c62703Revert "Select dropdown option with playwright api instead of manual click"

This reverts commit c7ab4c7ecf7b05f32a85568bce1a667ad8c62703.

* Wait for 5s after dropdown click

* Revert "Wait for 5s after dropdown click"

This reverts commit 847e9ad33fbfac99a947fc088e1c8c1c14c57853.

* Try forcing click

* Revert "Try forcing click"

This reverts commit 29b9fa1bda18dd3aba7853b20c6215d971bc826a.

* Force click on the dropdown and set viewport size bigger.

* Force click on the dropdown and set viewport size bigger.

* try force clicking option

* Skip container test on webkit

* Add branded browsers to e2e tests

---------

Co-authored-by: Ashley Stanton-Nurse <ashleyst@microsoft.com>
2024-07-30 15:41:41 -07:00

603 lines
21 KiB
TypeScript

import { CapabilityNames } from "Common/Constants";
import { Platform, updateConfigContext } from "ConfigContext";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
import TabsBase from "Explorer/Tabs/TabsBase";
import StoredProcedure from "Explorer/Tree/StoredProcedure";
import Trigger from "Explorer/Tree/Trigger";
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
import {
createDatabaseTreeNodes,
createResourceTokenTreeNodes,
createSampleDataTreeNodes,
} from "Explorer/Tree/treeNodeUtil";
import { useDatabases } from "Explorer/useDatabases";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { updateUserContext } from "UserContext";
import PromiseSource from "Utils/PromiseSource";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import ko from "knockout";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
jest.mock("Explorer/Explorer", () => {
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class MockExplorer {
onNewCollectionClicked = jest.fn();
}
return MockExplorer;
});
jest.mock("Explorer/Tree/StoredProcedure", () => {
let counter = 0;
class MockStoredProcedure {
id: () => string;
open = jest.fn();
delete = jest.fn();
constructor() {
this.id = () => `mockSproc${counter}`;
counter++;
}
}
return MockStoredProcedure;
});
jest.mock("Explorer/Tree/UserDefinedFunction", () => {
let counter = 0;
class MockUserDefinedFunction {
id: () => string;
open = jest.fn();
delete = jest.fn();
constructor() {
this.id = () => `mockUdf${counter}`;
counter++;
}
}
return MockUserDefinedFunction;
});
jest.mock("Explorer/Tree/Trigger", () => {
let counter = 0;
class MockTrigger {
id: () => string;
open = jest.fn();
delete = jest.fn();
constructor() {
this.id = () => `mockTrigger${counter}`;
counter++;
}
}
return MockTrigger;
});
jest.mock("Common/DatabaseAccountUtility", () => {
return {
isPublicInternetAccessAllowed: () => true,
};
});
// Defining this value outside the mock, AND prefixing the name with 'mock' is required by Jest's mocking logic.
let nextTabIndex = 1;
class MockTab extends TabsBase {
constructor(tabOptions: Pick<ViewModels.TabOptions, "tabKind"> & Partial<ViewModels.TabOptions>) {
super({
title: `Mock Tab ${nextTabIndex}`,
tabPath: `mockTabs/tab${nextTabIndex}`,
...tabOptions,
} as ViewModels.TabOptions);
nextTabIndex++;
}
onActivate = jest.fn();
}
/** A basic test collection that can be expanded on in tests. */
const baseCollection = {
container: new Explorer(),
databaseId: "testDatabase",
id: ko.observable<string>("testCollection"),
defaultTtl: ko.observable<number>(5),
analyticalStorageTtl: ko.observable<number>(undefined),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(),
indexingPolicy: ko.observable<DataModels.IndexingPolicy>({
automatic: true,
indexingMode: "consistent",
includedPaths: [],
excludedPaths: [],
}),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
usageSizeInKB: ko.observable(100),
offer: ko.observable<DataModels.Offer>({
autoscaleMaxThroughput: undefined,
manualThroughput: 10000,
minimumThroughput: 6000,
id: "offer",
offerReplacePending: false,
}),
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
{} as DataModels.ConflictResolutionPolicy,
),
changeFeedPolicy: ko.observable<DataModels.ChangeFeedPolicy>({} as DataModels.ChangeFeedPolicy),
geospatialConfig: ko.observable<DataModels.GeospatialConfig>({} as DataModels.GeospatialConfig),
getDatabase: () => {},
partitionKey: {
paths: [],
kind: "hash",
version: 2,
},
storedProcedures: ko.observableArray([]),
userDefinedFunctions: ko.observableArray([]),
triggers: ko.observableArray([]),
partitionKeyProperties: ["testPartitionKey"],
readSettings: () => {},
isCollectionExpanded: ko.observable(true),
onSettingsClick: jest.fn(),
onDocumentDBDocumentsClick: jest.fn(),
onNewQueryClick: jest.fn(),
onConflictsClick: jest.fn(),
onSchemaAnalyzerClick: jest.fn(),
} as unknown as ViewModels.Collection;
/** A basic test database that can be expanded on in tests */
const baseDatabase = {
container: new Explorer(),
id: ko.observable<string>("testDatabase"),
collections: ko.observableArray<ViewModels.Collection>([]),
isDatabaseShared: ko.pureComputed(() => false),
isDatabaseExpanded: ko.observable(true),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(),
expandDatabase: jest.fn().mockResolvedValue({}),
collapseDatabase: jest.fn(),
onSettingsClick: jest.fn(),
} as unknown as ViewModels.Database;
/** Configures app state so that useSelectedNode.getState().isDataNodeSelected() returns true for the provided arguments. */
function selectDataNode(
node: ViewModels.Database | ViewModels.CollectionBase,
subnodeKind?: ViewModels.CollectionTabKind,
) {
useSelectedNode.getState().setSelectedNode(node);
if (subnodeKind !== undefined) {
node.selectedSubnodeKind(subnodeKind);
useTabs.getState().activateNewTab(new MockTab({ tabKind: subnodeKind, node }));
}
}
describe("createSampleDataTreeNodes", () => {
let sampleDataResourceTokenCollection: ViewModels.Collection;
let nodes: TreeNode[];
beforeEach(() => {
jest.resetAllMocks();
const collection = { ...baseCollection };
useDatabases.setState({ sampleDataResourceTokenCollection: collection });
useSelectedNode.setState({ selectedNode: undefined });
sampleDataResourceTokenCollection = collection;
nodes = createSampleDataTreeNodes(sampleDataResourceTokenCollection);
});
it("creates the expected tree nodes", () => {
expect(nodes).toMatchSnapshot();
});
});
describe("createResourceTokenTreeNodes", () => {
let resourceTokenCollection: ViewModels.Collection;
let nodes: TreeNode[];
beforeEach(() => {
jest.resetAllMocks();
const collection = { ...baseCollection };
useDatabases.setState({ resourceTokenCollection: collection });
useSelectedNode.setState({ selectedNode: undefined });
resourceTokenCollection = collection;
nodes = createResourceTokenTreeNodes(resourceTokenCollection);
});
it("returns an empty node when collection is undefined or null", () => {
const snapshot = `
[
{
"children": [],
"isExpanded": true,
"label": "",
},
]
`;
expect(createResourceTokenTreeNodes(undefined)).toMatchInlineSnapshot(snapshot);
expect(createResourceTokenTreeNodes(null)).toMatchInlineSnapshot(snapshot);
});
it("creates the expected tree nodes", () => {
expect(nodes).toMatchSnapshot();
});
});
describe("createDatabaseTreeNodes", () => {
let explorer: Explorer;
let standardDb: ViewModels.Database;
let sharedDb: ViewModels.Database;
let giganticDb: ViewModels.Database;
let standardCollection: ViewModels.Collection;
let sampleItemsCollection: ViewModels.Collection;
let schemaCollection: ViewModels.Collection;
let conflictsCollection: ViewModels.Collection;
let sproc: StoredProcedure;
let udf: UserDefinedFunction;
let trigger: Trigger;
let refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
beforeEach(() => {
jest.resetAllMocks();
explorer = new Explorer();
standardDb = {
...baseDatabase,
id: ko.observable("standardDb"),
container: explorer,
} as ViewModels.Database;
sharedDb = {
...baseDatabase,
id: ko.observable("sharedDatabase"),
container: explorer,
isDatabaseShared: ko.pureComputed(() => true),
} as ViewModels.Database;
giganticDb = {
...baseDatabase,
id: ko.observable("giganticDatabase"),
container: explorer,
collectionsContinuationToken: "continuationToken",
} as ViewModels.Database;
standardCollection = {
...baseCollection,
id: ko.observable("standardCollection"),
container: explorer,
databaseId: standardDb.id(),
} as ViewModels.Collection;
// These classes are mocked, so the constructor args don't matter
sproc = new StoredProcedure(explorer, standardCollection, {} as never);
standardCollection.storedProcedures = ko.pureComputed(() => [sproc]);
udf = new UserDefinedFunction(explorer, standardCollection, {} as never);
standardCollection.userDefinedFunctions = ko.pureComputed(() => [udf]);
trigger = new Trigger(explorer, standardCollection, {} as never);
standardCollection.triggers = ko.pureComputed(() => [trigger]);
sampleItemsCollection = {
...baseCollection,
id: ko.observable("sampleItemsCollection"),
container: explorer,
databaseId: sharedDb.id(),
isSampleCollection: true,
} as ViewModels.Collection;
schemaCollection = {
...baseCollection,
id: ko.observable("schemaCollection"),
container: explorer,
databaseId: sharedDb.id(),
analyticalStorageTtl: ko.observable<number>(5),
schema: {
fields: [
{
path: "address.street",
dataType: { name: "string" },
hasNulls: false,
},
{
path: "address.line2",
dataType: { name: "string" },
hasNulls: true,
},
{
path: "address.zip",
dataType: { name: "number" },
hasNulls: false,
},
{
path: "orderId",
dataType: { name: "string" },
hasNulls: false,
},
],
} as unknown,
} as ViewModels.Collection;
conflictsCollection = {
...baseCollection,
id: ko.observable("conflictsCollection"),
rawDataModel: {
conflictResolutionPolicy: {
mode: "Custom",
conflictResolutionPath: "path",
conflictResolutionProcedure: "proc",
},
},
} as ViewModels.Collection;
standardDb.collections = ko.observableArray([standardCollection, conflictsCollection]);
sharedDb.collections = ko.observableArray([sampleItemsCollection]);
giganticDb.collections = ko.observableArray([schemaCollection]);
useDatabases.setState({
databases: [standardDb, sharedDb, giganticDb],
updateDatabase: jest.fn(),
});
useSelectedNode.setState({ selectedNode: undefined });
refreshActiveTab = jest.fn();
});
describe("using NoSQL API on Hosted Platform", () => {
let nodes: TreeNode[];
beforeEach(() => {
updateConfigContext({
platform: Platform.Hosted,
});
updateUserContext({
databaseAccount: {
properties: {
capabilities: [],
},
} as never,
});
nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab);
});
it("creates expected tree", () => {
expect(nodes).toMatchSnapshot();
});
});
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>]>([
["the SQL API, on Fabric", Platform.Fabric, false, { capabilities: [], enableMultipleWriteLocations: true }],
["the SQL API, on Portal", Platform.Portal, false, { capabilities: [], enableMultipleWriteLocations: true }],
[
"the Cassandra API, serverless, on Hosted",
Platform.Hosted,
false,
{
capabilities: [
{ name: CapabilityNames.EnableCassandra, description: "" },
{ name: CapabilityNames.EnableServerless, description: "" },
],
},
],
[
"the Mongo API, with Notebooks and Phoenix features, on Emulator",
Platform.Emulator,
true,
{
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
},
],
])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => {
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
updateConfigContext({ platform });
updateUserContext({
databaseAccount: {
properties: {
enableMultipleWriteLocations: true,
...dbAccountProperties,
},
} as unknown as DataModels.DatabaseAccount,
});
const nodes = createDatabaseTreeNodes(
explorer,
isNotebookEnabled,
useDatabases.getState().databases,
refreshActiveTab,
);
expect(nodes).toMatchSnapshot();
});
// The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes.
// They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle.
// The goal is to cover some key behaviors like loading child nodes, opening tabs/side panels, etc.
it("adds new collections to database as they appear", () => {
const nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab);
const giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
expect(giganticDbNode).toBeDefined();
expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]);
giganticDb.collections.push({
...baseCollection,
id: ko.observable("addedCollection"),
});
expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual([
"schemaCollection",
"addedCollection",
"load more",
]);
});
describe("the database node", () => {
let nodes: TreeNode[];
let standardDbNode: TreeNode;
let sharedDbNode: TreeNode;
let giganticDbNode: TreeNode;
beforeEach(() => {
updateConfigContext({ platform: Platform.Hosted });
updateUserContext({
databaseAccount: {
properties: {
capabilities: [],
},
} as unknown as DataModels.DatabaseAccount,
});
nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab);
standardDbNode = nodes.find((node) => node.label === standardDb.id());
sharedDbNode = nodes.find((node) => node.label === sharedDb.id());
giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
});
it("loads child nodes when expanded", async () => {
// Temporarily clear the child nodes to trigger the loading behavior
standardDbNode.children = [];
const expanding = new PromiseSource();
let expandCalled = false;
standardDb.expandDatabase = () => {
expandCalled = true;
return expanding.promise;
};
standardDbNode.onExpanded();
expect(useSelectedNode.getState().selectedNode).toBe(standardDb);
expect(standardDbNode.isLoading).toStrictEqual(true);
expect(expandCalled).toStrictEqual(true);
await expanding.resolveAndWait();
expect(standardDbNode.isLoading).toStrictEqual(false);
expect(useCommandBar.getState().contextButtons).toStrictEqual([]);
expect(refreshActiveTab).toHaveBeenCalled();
expect(useDatabases.getState().updateDatabase).toHaveBeenCalledWith(standardDb);
});
it("opens a New Container panel when 'New Container' option in context menu is clicked", () => {
const newContainerMenuItem = standardDbNode.contextMenu.find((item) => item.label === "New Container");
newContainerMenuItem.onClick();
expect(explorer.onNewCollectionClicked).toHaveBeenCalled();
});
it("opens a Delete Database panel when 'Delete Database' option in context menu is clicked", () => {
const deleteDatabaseMenuItem = standardDbNode.contextMenu.find((item) => item.label === "Delete Database");
deleteDatabaseMenuItem.onClick();
expect(useSidePanel.getState().headerText).toStrictEqual("Delete Database");
expect(useSidePanel.getState().panelContent.type).toStrictEqual(DeleteDatabaseConfirmationPanel);
});
describe("the Scale subnode", () => {
let scaleNode: TreeNode;
beforeEach(() => {
scaleNode = sharedDbNode.children.find((node) => node.label === "Scale");
});
it("is selected when Scale tab is open", () => {
expect(scaleNode.isSelected()).toStrictEqual(false);
selectDataNode(sharedDb, ViewModels.CollectionTabKind.DatabaseSettingsV2);
expect(scaleNode.isSelected()).toStrictEqual(true);
});
it("opens settings tab when clicked", () => {
expect(sharedDb.onSettingsClick).not.toHaveBeenCalled();
scaleNode.onClick();
expect(sharedDb.onSettingsClick).toHaveBeenCalled();
});
});
describe("the load more node", () => {
it("loads more collections when clicked", async () => {
const loadCollections = new PromiseSource();
let loadCalled = false;
giganticDb.loadCollections = () => {
loadCalled = true;
return loadCollections.promise;
};
const loadMoreNode = giganticDbNode.children.find((node) => node.label === "load more");
loadMoreNode.onClick();
expect(loadCalled).toStrictEqual(true);
await loadCollections.resolveAndWait();
expect(useDatabases.getState().updateDatabase).toHaveBeenCalledWith(giganticDb);
});
});
describe("the Collection subnode", () => {
let standardCollectionNode: TreeNode;
beforeEach(() => {
standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id());
});
it.each([
[
"for SQL API",
() => updateUserContext({ databaseAccount: { properties: {} } as unknown as DataModels.DatabaseAccount }),
],
[
"for Gremlin API",
() =>
updateUserContext({
databaseAccount: {
properties: { capabilities: [{ name: CapabilityNames.EnableGremlin, description: "" }] },
} as unknown as DataModels.DatabaseAccount,
}),
],
])("loads sprocs/udfs/triggers when expanded, %s", async () => {
standardCollection.loadStoredProcedures = jest.fn(() => Promise.resolve());
standardCollection.loadUserDefinedFunctions = jest.fn(() => Promise.resolve());
standardCollection.loadTriggers = jest.fn(() => Promise.resolve());
await standardCollectionNode.onExpanded();
expect(standardCollection.loadStoredProcedures).toHaveBeenCalled();
expect(standardCollection.loadUserDefinedFunctions).toHaveBeenCalled();
expect(standardCollection.loadTriggers).toHaveBeenCalled();
});
it.each([
["in Fabric", () => updateConfigContext({ platform: Platform.Fabric })],
[
"for Cassandra API",
() =>
updateUserContext({
databaseAccount: {
properties: { capabilities: [{ name: CapabilityNames.EnableCassandra, description: "" }] },
} as unknown as DataModels.DatabaseAccount,
}),
],
[
"for Mongo API",
() =>
updateUserContext({
databaseAccount: {
properties: { capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }] },
} as unknown as DataModels.DatabaseAccount,
}),
],
[
"for Tables API",
() =>
updateUserContext({
databaseAccount: {
properties: { capabilities: [{ name: CapabilityNames.EnableTable, description: "" }] },
} as unknown as DataModels.DatabaseAccount,
}),
],
])("does not load sprocs/udfs/triggers when expanded, %s", async (_, setup) => {
setup();
// Rebuild the nodes after changing the user/config context.
nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab);
standardDbNode = nodes.find((node) => node.label === standardDb.id());
standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id());
standardCollection.loadStoredProcedures = jest.fn(() => Promise.resolve());
standardCollection.loadUserDefinedFunctions = jest.fn(() => Promise.resolve());
standardCollection.loadTriggers = jest.fn(() => Promise.resolve());
await standardCollectionNode.onExpanded();
expect(standardCollection.loadStoredProcedures).not.toHaveBeenCalled();
expect(standardCollection.loadUserDefinedFunctions).not.toHaveBeenCalled();
expect(standardCollection.loadTriggers).not.toHaveBeenCalled();
});
});
});
});