mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-08 13:37:29 +01:00
feat: Add "Duplicate Tab" support for Items, Query, and Settings tabs (#2498)
* feat: Add "Duplicate Tab" support for Items, Query, and Settings tabs * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * added UT * Fix QueryTab useTabs import path for duplicate tab test mock --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { DocumentsTabV2 } from "./DocumentsTabV2";
|
||||
|
||||
jest.mock("hooks/useTabs", () => ({
|
||||
useTabs: {
|
||||
getState: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("UserContext", () => ({
|
||||
userContext: { apiType: "SQL" },
|
||||
}));
|
||||
|
||||
jest.mock("Explorer/Menus/CommandBar/CommandBarComponentAdapter", () => ({
|
||||
useCommandBar: { getState: jest.fn(() => ({ setContextButtons: jest.fn() })) },
|
||||
}));
|
||||
|
||||
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||
EditorReact: () => null,
|
||||
}));
|
||||
|
||||
const mockCollection = {
|
||||
id: ko.observable<string>("testContainer"),
|
||||
databaseId: "testDb",
|
||||
partitionKey: { paths: ["/pk"], kind: "Hash", version: 2 },
|
||||
selectedSubnodeKind: jest.fn(),
|
||||
container: {},
|
||||
} as unknown as ViewModels.Collection;
|
||||
|
||||
const buildTab = () =>
|
||||
new DocumentsTabV2({
|
||||
partitionKey: mockCollection.partitionKey,
|
||||
documentIds: ko.observableArray([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Items",
|
||||
collection: mockCollection,
|
||||
node: mockCollection,
|
||||
tabPath: "testDb>testContainer>Documents",
|
||||
});
|
||||
|
||||
describe("DocumentsTabV2.duplicateTab", () => {
|
||||
let activateNewTab: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
activateNewTab = jest.fn();
|
||||
(useTabs.getState as jest.Mock).mockReturnValue({ activateNewTab });
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it("calls activateNewTab with a new DocumentsTabV2 instance", () => {
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
expect(activateNewTab).toHaveBeenCalledTimes(1);
|
||||
const newTab = activateNewTab.mock.calls[0][0];
|
||||
expect(newTab).toBeInstanceOf(DocumentsTabV2);
|
||||
});
|
||||
|
||||
it("creates a duplicate with the same collection", () => {
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
const newTab = activateNewTab.mock.calls[0][0] as DocumentsTabV2;
|
||||
expect(newTab.collection).toBe(mockCollection);
|
||||
});
|
||||
|
||||
it("creates a duplicate with the same partitionKey", () => {
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
const newTab = activateNewTab.mock.calls[0][0] as DocumentsTabV2;
|
||||
expect(newTab.partitionKey).toEqual(mockCollection.partitionKey);
|
||||
});
|
||||
|
||||
it("creates a distinct tab instance", () => {
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
const newTab = activateNewTab.mock.calls[0][0];
|
||||
expect(newTab).not.toBe(tab);
|
||||
});
|
||||
|
||||
it("preserves the raw title (not the display title) to avoid double-prefixing", () => {
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
const newTab = activateNewTab.mock.calls[0][0] as DocumentsTabV2;
|
||||
// The original tabTitle() is "testC…Items" (collection "testContainer" is > 8 chars so it's truncated).
|
||||
// If duplicateTab() incorrectly used tabTitle() as the new title, the duplicate's tabTitle()
|
||||
// would double-prefix to "testC…testC…Items". Using the raw title "Items" keeps it "testC…Items".
|
||||
expect(newTab.tabTitle()).toBe("testC\u2026Items");
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,8 @@ import { userContext } from "UserContext";
|
||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import { Allotment } from "allotment";
|
||||
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import ko from "knockout";
|
||||
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { format } from "react-string-format";
|
||||
import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
||||
@@ -176,6 +178,25 @@ export class DocumentsTabV2 extends TabsBase {
|
||||
};
|
||||
}
|
||||
|
||||
public canDuplicate(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public duplicateTab(): void {
|
||||
const newTab = new DocumentsTabV2({
|
||||
partitionKey: this.partitionKey,
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: this.title,
|
||||
collection: this.collection,
|
||||
node: this.collection,
|
||||
tabPath: `${this.collection.databaseId}>${this.collection.id()}>Documents`,
|
||||
isPreferredApiMongoDB: userContext.apiType === "Mongo",
|
||||
resourceTokenPartitionKey: this.resourceTokenPartitionKey,
|
||||
});
|
||||
useTabs.getState().activateNewTab(newTab);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<DocumentsTabComponent
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ActionType, TabKind } from "Contracts/ActionContracts";
|
||||
import React from "react";
|
||||
import MongoUtility from "../../../Common/MongoUtility";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NewQueryTab } from "../QueryTab/QueryTab";
|
||||
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../QueryTab/QueryTabComponent";
|
||||
@@ -67,6 +68,30 @@ export class NewMongoQueryTab extends NewQueryTab {
|
||||
return MongoUtility.tojson(value, undefined, false);
|
||||
}
|
||||
|
||||
public canDuplicate(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public duplicateTab(): void {
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||
const queryText = this.iTabAccessor?.onSaveClickEvent() ?? this.persistedState?.query?.text ?? "";
|
||||
const newTab = new NewMongoQueryTab(
|
||||
{
|
||||
tabKind: ViewModels.CollectionTabKind.Query,
|
||||
title: `Query ${id}`,
|
||||
tabPath: "",
|
||||
collection: this.collection,
|
||||
node: this.collection,
|
||||
queryText,
|
||||
partitionKey: this.partitionKey,
|
||||
splitterDirection: this.persistedState?.splitterDirection,
|
||||
queryViewSizePercent: this.persistedState?.queryViewSizePercent,
|
||||
},
|
||||
this.mongoQueryTabProps,
|
||||
);
|
||||
useTabs.getState().activateNewTab(newTab);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return <QueryTabComponent {...this.iMongoQueryTabComponentProps} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { IQueryTabProps, NewQueryTab } from "./QueryTab";
|
||||
|
||||
jest.mock("hooks/useTabs", () => ({
|
||||
useTabs: {
|
||||
getState: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("Explorer/Menus/CommandBar/CommandBarComponentAdapter", () => ({
|
||||
useCommandBar: { getState: jest.fn(() => ({ setContextButtons: jest.fn() })) },
|
||||
}));
|
||||
|
||||
jest.mock("Shared/AppStatePersistenceUtility", () => ({
|
||||
loadState: jest.fn(),
|
||||
AppStateComponentNames: {},
|
||||
readSubComponentState: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("Common/MessageHandler", () => ({
|
||||
sendMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockCollection = {
|
||||
id: ko.observable<string>("testContainer"),
|
||||
databaseId: "testDb",
|
||||
partitionKey: { paths: ["/pk"], kind: "Hash", version: 2 },
|
||||
selectedSubnodeKind: jest.fn(),
|
||||
container: {},
|
||||
} as unknown as ViewModels.Collection;
|
||||
|
||||
const mockProps = { container: {} as IQueryTabProps["container"] };
|
||||
|
||||
const buildTab = (queryText = "SELECT * FROM c") =>
|
||||
new NewQueryTab(
|
||||
{
|
||||
tabKind: ViewModels.CollectionTabKind.Query,
|
||||
title: "Query 1",
|
||||
tabPath: "",
|
||||
collection: mockCollection,
|
||||
node: mockCollection,
|
||||
queryText,
|
||||
partitionKey: mockCollection.partitionKey,
|
||||
},
|
||||
mockProps,
|
||||
);
|
||||
|
||||
describe("NewQueryTab.duplicateTab", () => {
|
||||
let activateNewTab: jest.Mock;
|
||||
let getTabs: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
activateNewTab = jest.fn();
|
||||
getTabs = jest.fn().mockReturnValue([]);
|
||||
(useTabs.getState as jest.Mock).mockReturnValue({ activateNewTab, getTabs });
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it("calls activateNewTab with a new NewQueryTab instance", () => {
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
expect(activateNewTab).toHaveBeenCalledTimes(1);
|
||||
const newTab = activateNewTab.mock.calls[0][0];
|
||||
expect(newTab).toBeInstanceOf(NewQueryTab);
|
||||
});
|
||||
|
||||
it("preserves the current query text in the duplicate", () => {
|
||||
const queryText = "SELECT * FROM c WHERE c.id = '123'";
|
||||
const tab = buildTab(queryText);
|
||||
tab.duplicateTab();
|
||||
|
||||
const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
|
||||
expect(newTab.iQueryTabComponentProps.queryText).toBe(queryText);
|
||||
});
|
||||
|
||||
it("creates a duplicate with the same collection", () => {
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
|
||||
expect(newTab.collection).toBe(mockCollection);
|
||||
});
|
||||
|
||||
it("creates a distinct tab instance", () => {
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
const newTab = activateNewTab.mock.calls[0][0];
|
||||
expect(newTab).not.toBe(tab);
|
||||
});
|
||||
|
||||
it("assigns an auto-incremented title based on existing query tabs", () => {
|
||||
getTabs.mockReturnValue([{}, {}]); // 2 existing tabs → new title = "Query 3"
|
||||
const tab = buildTab();
|
||||
tab.duplicateTab();
|
||||
|
||||
const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
|
||||
expect(newTab.tabTitle()).toContain("Query 3");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,8 @@ import { MessageTypes } from "Contracts/MessageTypes";
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../../Tabs/QueryTab/QueryTabComponent";
|
||||
import TabsBase from "../TabsBase";
|
||||
@@ -72,6 +73,30 @@ export class NewQueryTab extends TabsBase {
|
||||
});
|
||||
}
|
||||
|
||||
public canDuplicate(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public duplicateTab(): void {
|
||||
const queryText = this.iTabAccessor?.onSaveClickEvent() ?? this.persistedState?.query?.text ?? "";
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||
const newTab = new NewQueryTab(
|
||||
{
|
||||
tabKind: ViewModels.CollectionTabKind.Query,
|
||||
title: `Query ${id}`,
|
||||
tabPath: "",
|
||||
collection: this.collection,
|
||||
node: this.collection,
|
||||
queryText,
|
||||
partitionKey: this.partitionKey,
|
||||
splitterDirection: this.persistedState?.splitterDirection,
|
||||
queryViewSizePercent: this.persistedState?.queryViewSizePercent,
|
||||
},
|
||||
this.props,
|
||||
);
|
||||
useTabs.getState().activateNewTab(newTab);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return <QueryTabComponent {...this.iQueryTabComponentProps} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Spinner, SpinnerSize, TooltipHost } from "@fluentui/react";
|
||||
import { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger } from "@fluentui/react-components";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
@@ -10,6 +11,7 @@ import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { Keys, t } from "Localization";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
@@ -85,8 +87,9 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
focusTab.current.focus();
|
||||
}
|
||||
}, [active]);
|
||||
return (
|
||||
const liElement = (
|
||||
<li
|
||||
data-test={`TabNav:${tab !== undefined ? tab.tabId : ReactTabKind[tabKind!]}`}
|
||||
onMouseOver={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
className={active ? "active tabList" : "tabList"}
|
||||
@@ -155,6 +158,22 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (!tab?.canDuplicate()) {
|
||||
return liElement;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu openOnContext>
|
||||
<MenuTrigger disableButtonEnhancement>{liElement}</MenuTrigger>
|
||||
<MenuPopover>
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => tab.duplicateTab()}>{t(Keys.tabs.tabMenu.duplicateTab)}</MenuItem>
|
||||
<MenuItem onClick={() => tab.onCloseTabButtonClick()}>{t(Keys.tabs.tabMenu.closeTab)}</MenuItem>
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
const onKeyPressReactTabClose = (e: KeyboardEvent, tabKind: ReactTabKind): void => {
|
||||
|
||||
@@ -64,6 +64,14 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
public getPersistedState = (): OpenTab | null => this.persistedState;
|
||||
public triggerPersistState: () => void = undefined;
|
||||
|
||||
public canDuplicate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public duplicateTab(): void {
|
||||
// Subclasses override this to support tab duplication
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): void {
|
||||
useTabs.getState().closeTab(this);
|
||||
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
|
||||
|
||||
@@ -316,6 +316,10 @@
|
||||
"deleteUdf": "Delete User Defined Function"
|
||||
},
|
||||
"tabs": {
|
||||
"tabMenu": {
|
||||
"duplicateTab": "Duplicate tab",
|
||||
"closeTab": "Close tab"
|
||||
},
|
||||
"documents": {
|
||||
"newItem": "New Item",
|
||||
"newDocument": "New Document",
|
||||
|
||||
+10
@@ -616,6 +616,16 @@ export class DataExplorer {
|
||||
await page.goto(url);
|
||||
return DataExplorer.waitForExplorer(page);
|
||||
}
|
||||
|
||||
/** Returns the tab navigation <li> element for the given tab ID or React tab name (e.g. "tab0", "Home") */
|
||||
tabNavHeader(tabId: string): Locator {
|
||||
return this.frame.getByTestId(`TabNav:${tabId}`);
|
||||
}
|
||||
|
||||
/** Returns a context menu item in the tab right-click menu by visible label */
|
||||
tabContextMenuItem(label: string): Locator {
|
||||
return this.frame.getByRole("menuitem", { name: label });
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForApiResponse(
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { DataExplorer, TestAccount } from "../fx";
|
||||
import { createTestSQLContainer, TestContainerContext } from "../testData";
|
||||
|
||||
let context: TestContainerContext = null!;
|
||||
let explorer: DataExplorer = null!;
|
||||
|
||||
test.beforeAll("Create Test Database", async () => {
|
||||
context = await createTestSQLContainer({ includeTestData: false });
|
||||
});
|
||||
|
||||
test.afterAll("Delete Test Database", async () => {
|
||||
await context?.dispose();
|
||||
});
|
||||
|
||||
test.beforeEach("Open Data Explorer", async ({ page }) => {
|
||||
explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||
});
|
||||
|
||||
test("Duplicate Items tab opens a second Items tab", async () => {
|
||||
// Open Items tab
|
||||
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
|
||||
await containerNode.expand();
|
||||
const itemsNode = await explorer.waitForContainerItemsNode(context.database.id, context.container.id);
|
||||
await itemsNode.element.click();
|
||||
|
||||
const documentsTab = explorer.documentsTab("tab0");
|
||||
await documentsTab.documentsFilter.waitFor({ timeout: 30_000 });
|
||||
|
||||
// Right-click the tab nav header
|
||||
await explorer.tabNavHeader("tab0").click({ button: "right" });
|
||||
|
||||
// "Duplicate tab" should be visible in the context menu
|
||||
const duplicateMenuItem = explorer.tabContextMenuItem("Duplicate tab");
|
||||
await expect(duplicateMenuItem).toBeVisible();
|
||||
await duplicateMenuItem.click();
|
||||
|
||||
// A second tab should appear
|
||||
const tab1 = explorer.tab("tab1");
|
||||
await expect(tab1).toBeAttached({ timeout: 30_000 });
|
||||
|
||||
// The duplicated tab should also show the Documents content
|
||||
const duplicatedTab = explorer.documentsTab("tab1");
|
||||
await duplicatedTab.documentsFilter.waitFor({ timeout: 30_000 });
|
||||
});
|
||||
|
||||
test("Duplicate Query tab preserves query text in new tab", async () => {
|
||||
// Open a new SQL query tab via container context menu
|
||||
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
|
||||
await containerNode.openContextMenu();
|
||||
await containerNode.contextMenuItem("New SQL Query").click();
|
||||
|
||||
const queryTab = explorer.queryTab("tab0");
|
||||
const editor = queryTab.editor();
|
||||
await editor.locator.waitFor({ timeout: 30_000 });
|
||||
|
||||
// Type a custom query
|
||||
const customQuery = 'SELECT * FROM c WHERE c.id = "duplicate-query-test"';
|
||||
await editor.setText(customQuery);
|
||||
|
||||
// Right-click the tab nav header
|
||||
await explorer.tabNavHeader("tab0").click({ button: "right" });
|
||||
|
||||
const duplicateMenuItem = explorer.tabContextMenuItem("Duplicate tab");
|
||||
await expect(duplicateMenuItem).toBeVisible();
|
||||
await duplicateMenuItem.click();
|
||||
|
||||
// Second query tab should appear
|
||||
const tab1 = explorer.tab("tab1");
|
||||
await expect(tab1).toBeAttached({ timeout: 30_000 });
|
||||
|
||||
// The duplicated tab should contain the same query text
|
||||
const duplicatedQueryTab = explorer.queryTab("tab1");
|
||||
await duplicatedQueryTab.editor().locator.waitFor({ timeout: 30_000 });
|
||||
const editorText = await duplicatedQueryTab.editor().text();
|
||||
expect(editorText).toContain("duplicate-query-test");
|
||||
});
|
||||
|
||||
test("Right-click context menu does not appear for the Home tab", async () => {
|
||||
// The Home tab (ReactTabKind) is never duplicable — no context menu should appear
|
||||
await explorer.tabNavHeader("Home").click({ button: "right" });
|
||||
|
||||
// Neither menu item should be visible
|
||||
await expect(explorer.tabContextMenuItem("Duplicate tab")).not.toBeVisible();
|
||||
await expect(explorer.tabContextMenuItem("Close tab")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Close tab from right-click menu closes the tab", async () => {
|
||||
// Open Items tab
|
||||
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
|
||||
await containerNode.expand();
|
||||
const itemsNode = await explorer.waitForContainerItemsNode(context.database.id, context.container.id);
|
||||
await itemsNode.element.click();
|
||||
|
||||
const documentsTab = explorer.documentsTab("tab0");
|
||||
await documentsTab.documentsFilter.waitFor({ timeout: 30_000 });
|
||||
|
||||
// Right-click the tab nav header and close the tab
|
||||
await explorer.tabNavHeader("tab0").click({ button: "right" });
|
||||
await explorer.tabContextMenuItem("Close tab").click();
|
||||
|
||||
// The tab pane should be removed
|
||||
await expect(explorer.tab("tab0")).not.toBeAttached({ timeout: 15_000 });
|
||||
});
|
||||
Reference in New Issue
Block a user