Compare commits

...

7 Commits

Author SHA1 Message Date
Sung-Hyun Kang
70ed0c79e1 Add data-testid 2025-12-11 10:25:28 -06:00
Sung-Hyun Kang
c2785ace83 Fix unit tests 2025-12-10 17:03:04 -06:00
Sung-Hyun Kang
bb5d3bfc42 Add data-testid to components for testing 2025-12-10 16:51:09 -06:00
Sung-Hyun Kang
98a21f2fb1 Fix cassandra tests 2025-12-10 11:19:09 -06:00
Sung-Hyun Kang
945c457bd5 Added different container creation playwirhgt tests 2025-12-09 19:07:54 -06:00
sunghyunkang1111
5b7d1a74af Added health metrics for application load and database load (#2257)
* Added health metrics for application load

* Added health metrics for application load

* Fix unit tests

* Added more metrics

* Added few comments

* Added DatabaseLoad Scenario and address comments

* Fix unit tests

* fix unit tests

* Fix unit tests

* fix unit tests

* fix the mock

* Fix unit tests
2025-12-09 14:14:35 -06:00
jawelton74
8c0e6da377 Exclude the obj directory when creating the Nuget packages. (#2277) 2025-12-09 10:31:59 -08:00
70 changed files with 1825 additions and 429 deletions

View File

@@ -19,6 +19,6 @@
</frameworkAssemblies> </frameworkAssemblies>
</metadata> </metadata>
<files> <files>
<file src="**\*" target="content"/> <file src="**\*" exclude="obj\**\*" target="content"/>
</files> </files>
</package> </package>

6
package-lock.json generated
View File

@@ -116,6 +116,7 @@
"tinykeys": "2.1.0", "tinykeys": "2.1.0",
"underscore": "1.12.1", "underscore": "1.12.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"web-vitals": "4.2.4",
"uuid": "9.0.0", "uuid": "9.0.0",
"zustand": "3.5.0" "zustand": "3.5.0"
}, },
@@ -35930,6 +35931,11 @@
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
}, },
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"

View File

@@ -111,6 +111,7 @@
"tinykeys": "2.1.0", "tinykeys": "2.1.0",
"underscore": "1.12.1", "underscore": "1.12.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"web-vitals": "4.2.4",
"uuid": "9.0.0", "uuid": "9.0.0",
"zustand": "3.5.0" "zustand": "3.5.0"
}, },

View File

@@ -14,7 +14,7 @@ export default defineConfig({
trace: "off", trace: "off",
video: "off", video: "off",
screenshot: "on", screenshot: "on",
testIdAttribute: "data-test", testIdAttribute: "data-testid",
contextOptions: { contextOptions: {
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
}, },

View File

@@ -76,12 +76,10 @@ const mockedRbacUtils = RbacUtils as jest.Mocked<typeof RbacUtils>;
const mockedCopyJobPrerequisitesCache = CopyJobPrerequisitesCacheModule as jest.Mocked< const mockedCopyJobPrerequisitesCache = CopyJobPrerequisitesCacheModule as jest.Mocked<
typeof CopyJobPrerequisitesCacheModule typeof CopyJobPrerequisitesCacheModule
>; >;
interface TestWrapperProps { interface TestWrapperProps {
state: CopyJobContextState; state: CopyJobContextState;
onResult?: (result: PermissionGroupConfig[]) => void; onResult?: (result: PermissionGroupConfig[]) => void;
} }
const TestWrapper: React.FC<TestWrapperProps> = ({ state, onResult }) => { const TestWrapper: React.FC<TestWrapperProps> = ({ state, onResult }) => {
const result = usePermissionSections(state); const result = usePermissionSections(state);

View File

@@ -214,9 +214,9 @@ export const Dialog: FC = () => {
{contentHtml} {contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />} {progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter> <DialogFooter>
<PrimaryButton {...primaryButtonProps} data-test={`DialogButton:${primaryButtonText}`} /> <PrimaryButton {...primaryButtonProps} data-testid={`DialogButton:${primaryButtonText}`} />
{secondaryButtonProps && ( {secondaryButtonProps && (
<DefaultButton {...secondaryButtonProps} data-test={`DialogButton:${secondaryButtonText}`} /> <DefaultButton {...secondaryButtonProps} data-testid={`DialogButton:${secondaryButtonText}`} />
)} )}
</DialogFooter> </DialogFooter>
</FluentDialog> </FluentDialog>

View File

@@ -137,7 +137,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} /> <Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
)} )}
<div <div
data-test="EditorReact/Host/Unloaded" data-testid="EditorReact/Host/Unloaded"
className={this.props.className || "jsonEditor"} className={this.props.className || "jsonEditor"}
style={this.props.monacoContainerStyles} style={this.props.monacoContainerStyles}
ref={(elt: HTMLElement) => this.setRef(elt)} ref={(elt: HTMLElement) => this.setRef(elt)}
@@ -148,7 +148,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
this.editor = editor; this.editor = editor;
this.rootNode.dataset["test"] = "EditorReact/Host/Loaded"; this.rootNode.dataset["testid"] = "EditorReact/Host/Loaded";
// In development, we want to be able to access the editor instance from the console // In development, we want to be able to access the editor instance from the console
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {

View File

@@ -193,7 +193,7 @@ export const InputDataList: FC<InputDataListProps> = ({
<> <>
<Input <Input
id="filterInput" id="filterInput"
data-test={"DocumentsTab/FilterInput"} data-testid={"DocumentsTab/FilterInput"}
ref={inputRef} ref={inputRef}
type="text" type="text"
size="small" size="small"

View File

@@ -209,6 +209,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
checked={isAutoscaleSelected} checked={isAutoscaleSelected}
type="radio" type="radio"
role="radio" role="radio"
data-testid="ThroughputInput/ThroughputMode:Autoscale"
tabIndex={0} tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")} onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/> />
@@ -224,6 +225,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
type="radio" type="radio"
aria-required={true} aria-required={true}
role="radio" role="radio"
data-testid="ThroughputInput/ThroughputMode:Manual"
tabIndex={0} tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")} onChange={(e) => handleOnChangeMode(e, "Manual")}
/> />
@@ -286,7 +288,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Stack> </Stack>
<TextField <TextField
id="autoscaleRUValueField" id="autoscaleRUValueField"
data-test="autoscaleRUInput" data-testid="ThroughputInput/AutoscaleRUInput"
type="number" type="number"
styles={{ styles={{
fieldGroup: { width: 100, height: 27, flexShrink: 0 }, fieldGroup: { width: 100, height: 27, flexShrink: 0 },
@@ -352,6 +354,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
} }
> >
<TextField <TextField
data-testid="ThroughputInput/ManualThroughputInput"
type="number" type="number"
styles={{ styles={{
fieldGroup: { width: 300, height: 27 }, fieldGroup: { width: 300, height: 27 },

View File

@@ -682,6 +682,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-required={true} aria-required={true}
checked={true} checked={true}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
data-testid="ThroughputInput/ThroughputMode:Autoscale"
id="Autoscale-input" id="Autoscale-input"
onChange={[Function]} onChange={[Function]}
role="radio" role="radio"
@@ -699,6 +700,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-required={true} aria-required={true}
checked={false} checked={false}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
data-testid="ThroughputInput/ThroughputMode:Manual"
id="Manual-input" id="Manual-input"
onChange={[Function]} onChange={[Function]}
role="radio" role="radio"
@@ -2144,7 +2146,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Container max RU/s" ariaLabel="Container max RU/s"
data-test="autoscaleRUInput" data-testid="ThroughputInput/AutoscaleRUInput"
errorMessage="" errorMessage=""
id="autoscaleRUValueField" id="autoscaleRUValueField"
key=".0:$.$.1" key=".0:$.$.1"
@@ -2171,7 +2173,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
> >
<TextFieldBase <TextFieldBase
ariaLabel="Container max RU/s" ariaLabel="Container max RU/s"
data-test="autoscaleRUInput" data-testid="ThroughputInput/AutoscaleRUInput"
deferredValidationTime={200} deferredValidationTime={200}
errorMessage="" errorMessage=""
id="autoscaleRUValueField" id="autoscaleRUValueField"
@@ -2472,7 +2474,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-invalid={false} aria-invalid={false}
aria-label="Container max RU/s" aria-label="Container max RU/s"
className="ms-TextField-field field-124" className="ms-TextField-field field-124"
data-test="autoscaleRUInput" data-testid="ThroughputInput/AutoscaleRUInput"
id="autoscaleRUValueField" id="autoscaleRUValueField"
max="9007199254740991" max="9007199254740991"
min={1000} min={1000}

View File

@@ -139,7 +139,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
const contextMenuItems = (node.contextMenu ?? []).map((menuItem) => ( const contextMenuItems = (node.contextMenu ?? []).map((menuItem) => (
<MenuItem <MenuItem
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`} data-testid={`TreeNode/ContextMenuItem:${menuItem.label}`}
disabled={menuItem.isDisabled} disabled={menuItem.isDisabled}
key={menuItem.label} key={menuItem.label}
onClick={() => menuItem.onClick(contextMenuRef)} onClick={() => menuItem.onClick(contextMenuRef)}
@@ -160,14 +160,14 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
const expandIcon = isLoading ? ( const expandIcon = isLoading ? (
<Spinner size="extra-tiny" /> <Spinner size="extra-tiny" />
) : !isBranch ? undefined : openItems.includes(treeNodeId) ? ( ) : !isBranch ? undefined : openItems.includes(treeNodeId) ? (
<ChevronDown20Regular data-test="TreeNode/CollapseIcon" /> <ChevronDown20Regular data-testid="TreeNode/CollapseIcon" />
) : ( ) : (
<ChevronRight20Regular data-text="TreeNode/ExpandIcon" /> <ChevronRight20Regular data-testid="TreeNode/ExpandIcon" />
); );
const treeItem = ( const treeItem = (
<TreeItem <TreeItem
data-test={`TreeNodeContainer:${treeNodeId}`} data-testid={`TreeNodeContainer:${treeNodeId}`}
value={treeNodeId} value={treeNodeId}
itemType={isBranch ? "branch" : "leaf"} itemType={isBranch ? "branch" : "leaf"}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
@@ -179,7 +179,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
shouldShowAsSelected && treeStyles.selectedItem, shouldShowAsSelected && treeStyles.selectedItem,
node.className && treeStyles[node.className], node.className && treeStyles[node.className],
)} )}
data-test={`TreeNode:${treeNodeId}`} data-testid={`TreeNode:${treeNodeId}`}
actions={ actions={
contextMenuItems.length > 0 && { contextMenuItems.length > 0 && {
className: treeStyles.actionsButtonContainer, className: treeStyles.actionsButtonContainer,
@@ -189,13 +189,13 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
<Button <Button
aria-label="More options" aria-label="More options"
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)} className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
data-test="TreeNode/ContextMenuTrigger" data-testid="TreeNode/ContextMenuTrigger"
appearance="subtle" appearance="subtle"
ref={contextMenuRef} ref={contextMenuRef}
icon={<MoreHorizontal20Regular />} icon={<MoreHorizontal20Regular />}
/> />
</MenuTrigger> </MenuTrigger>
<MenuPopover data-test={`TreeNode/ContextMenu:${treeNodeId}`}> <MenuPopover data-testid={`TreeNode/ContextMenu:${treeNodeId}`}>
<MenuList>{contextMenuItems}</MenuList> <MenuList>{contextMenuItems}</MenuList>
</MenuPopover> </MenuPopover>
</Menu> </Menu>
@@ -208,7 +208,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
<span className={treeStyles.nodeLabel}>{node.label}</span> <span className={treeStyles.nodeLabel}>{node.label}</span>
</TreeItemLayout> </TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && ( {!node.isLoading && node.children?.length > 0 && (
<Tree data-test={`Tree:${treeNodeId}`} className={treeStyles.tree}> <Tree data-testid={`Tree:${treeNodeId}`} className={treeStyles.tree}>
{getSortedChildren(node).map((childNode: TreeNode) => ( {getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent <TreeNodeComponent
openItems={openItems} openItems={openItems}

View File

@@ -3,7 +3,7 @@
exports[`TreeNodeComponent does not render children if the node is loading 1`] = ` exports[`TreeNodeComponent does not render children if the node is loading 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -11,10 +11,10 @@ exports[`TreeNodeComponent does not render children if the node is loading 1`] =
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -112,7 +112,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -122,7 +122,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level={0} aria-level={0}
className="fui-TreeItem r15xhw3a" className="fui-TreeItem r15xhw3a"
data-fui-tree-item-value="root" data-fui-tree-item-value="root"
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
onChange={[Function]} onChange={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -144,7 +144,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -164,7 +164,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -173,7 +173,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -220,13 +220,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="0" aria-level="0"
class="fui-TreeItem r15xhw3a" class="fui-TreeItem r15xhw3a"
data-fui-tree-item-value="root" data-fui-tree-item-value="root"
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -235,7 +235,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -270,7 +270,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</div> </div>
<div <div
class="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" class="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-test="Tree:root" data-testid="Tree:root"
role="tree" role="tree"
> >
<div <div
@@ -278,13 +278,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child1Label" data-fui-tree-item-value="root/child1Label"
data-test="TreeNodeContainer:root/child1Label" data-testid="TreeNodeContainer:root/child1Label"
role="treeitem" role="treeitem"
tabindex="0" tabindex="0"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-testid="TreeNode:root/child1Label"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -293,7 +293,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -332,13 +332,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child2LoadingLabel" data-fui-tree-item-value="root/child2LoadingLabel"
data-test="TreeNodeContainer:root/child2LoadingLabel" data-testid="TreeNodeContainer:root/child2LoadingLabel"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-testid="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -347,7 +347,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -385,13 +385,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child3ExpandingLabel" data-fui-tree-item-value="root/child3ExpandingLabel"
data-test="TreeNodeContainer:root/child3ExpandingLabel" data-testid="TreeNodeContainer:root/child3ExpandingLabel"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-testid="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -441,10 +441,10 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -457,19 +457,19 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
> >
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -506,7 +506,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-test="Tree:root" data-testid="Tree:root"
> >
<TreeProvider <TreeProvider
value={ value={
@@ -574,7 +574,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-test="Tree:root" data-testid="Tree:root"
role="tree" role="tree"
> >
<TreeNodeComponent <TreeNodeComponent
@@ -610,7 +610,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root/child1Label" data-testid="TreeNodeContainer:root/child1Label"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root/child1Label" value="root/child1Label"
@@ -620,7 +620,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level={1} aria-level={1}
className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child1Label" data-fui-tree-item-value="root/child1Label"
data-test="TreeNodeContainer:root/child1Label" data-testid="TreeNodeContainer:root/child1Label"
onChange={[Function]} onChange={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -642,7 +642,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -662,7 +662,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-testid="TreeNode:root/child1Label"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -671,7 +671,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -718,13 +718,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child1Label" data-fui-tree-item-value="root/child1Label"
data-test="TreeNodeContainer:root/child1Label" data-testid="TreeNodeContainer:root/child1Label"
role="treeitem" role="treeitem"
tabindex="0" tabindex="0"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-testid="TreeNode:root/child1Label"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -733,7 +733,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -775,10 +775,10 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-testid="TreeNode:root/child1Label"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -791,19 +791,19 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-testid="TreeNode:root/child1Label"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
> >
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -840,7 +840,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-test="Tree:root/child1Label" data-testid="Tree:root/child1Label"
> >
<TreeProvider <TreeProvider
value={ value={
@@ -881,7 +881,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root/child2LoadingLabel" data-testid="TreeNodeContainer:root/child2LoadingLabel"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root/child2LoadingLabel" value="root/child2LoadingLabel"
@@ -891,7 +891,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level={1} aria-level={1}
className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child2LoadingLabel" data-fui-tree-item-value="root/child2LoadingLabel"
data-test="TreeNodeContainer:root/child2LoadingLabel" data-testid="TreeNodeContainer:root/child2LoadingLabel"
onChange={[Function]} onChange={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -913,7 +913,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -933,7 +933,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-testid="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -942,7 +942,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -989,13 +989,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child2LoadingLabel" data-fui-tree-item-value="root/child2LoadingLabel"
data-test="TreeNodeContainer:root/child2LoadingLabel" data-testid="TreeNodeContainer:root/child2LoadingLabel"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-testid="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -1004,7 +1004,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -1046,10 +1046,10 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-testid="TreeNode:root/child2LoadingLabel"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -1062,19 +1062,19 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-testid="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
> >
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -1140,7 +1140,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root/child3ExpandingLabel" data-testid="TreeNodeContainer:root/child3ExpandingLabel"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root/child3ExpandingLabel" value="root/child3ExpandingLabel"
@@ -1149,7 +1149,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level={1} aria-level={1}
className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" className="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child3ExpandingLabel" data-fui-tree-item-value="root/child3ExpandingLabel"
data-test="TreeNodeContainer:root/child3ExpandingLabel" data-testid="TreeNodeContainer:root/child3ExpandingLabel"
onChange={[Function]} onChange={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -1188,7 +1188,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-testid="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -1240,13 +1240,13 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
aria-level="1" aria-level="1"
class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd" class="fui-TreeItem r15xhw3a ___jer8zr0_6no3ah0 f10bgyvd"
data-fui-tree-item-value="root/child3ExpandingLabel" data-fui-tree-item-value="root/child3ExpandingLabel"
data-test="TreeNodeContainer:root/child3ExpandingLabel" data-testid="TreeNodeContainer:root/child3ExpandingLabel"
role="treeitem" role="treeitem"
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-testid="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden="true" aria-hidden="true"
@@ -1294,7 +1294,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-testid="TreeNode:root/child3ExpandingLabel"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1305,7 +1305,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-testid="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden={true} aria-hidden={true}
@@ -1345,7 +1345,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
exports[`TreeNodeComponent renders a loading spinner if the node is loading: loaded 1`] = ` exports[`TreeNodeComponent renders a loading spinner if the node is loading: loaded 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1353,7 +1353,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1374,7 +1374,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
exports[`TreeNodeComponent renders a loading spinner if the node is loading: loading 1`] = ` exports[`TreeNodeComponent renders a loading spinner if the node is loading: loading 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1382,7 +1382,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
expandIcon={ expandIcon={
<Spinner <Spinner
size="extra-tiny" size="extra-tiny"
@@ -1408,7 +1408,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
exports[`TreeNodeComponent renders a node as expandable if it has empty, but defined, children array 1`] = ` exports[`TreeNodeComponent renders a node as expandable if it has empty, but defined, children array 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1416,10 +1416,10 @@ exports[`TreeNodeComponent renders a node as expandable if it has empty, but def
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -1450,7 +1450,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
> >
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1468,22 +1468,22 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
appearance="subtle" appearance="subtle"
aria-label="More options" aria-label="More options"
className="___1pg0eu5_pgl3ex0 f1twygmj" className="___1pg0eu5_pgl3ex0 f1twygmj"
data-test="TreeNode/ContextMenuTrigger" data-testid="TreeNode/ContextMenuTrigger"
icon={<MoreHorizontal20Regular />} icon={<MoreHorizontal20Regular />}
/> />
</MenuTrigger> </MenuTrigger>
<MenuPopover <MenuPopover
data-test="TreeNode/ContextMenu:root" data-testid="TreeNode/ContextMenu:root"
> >
<MenuList> <MenuList>
<MenuItem <MenuItem
data-test="TreeNode/ContextMenuItem:enabledItem" data-testid="TreeNode/ContextMenuItem:enabledItem"
onClick={[Function]} onClick={[Function]}
> >
enabledItem enabledItem
</MenuItem> </MenuItem>
<MenuItem <MenuItem
data-test="TreeNode/ContextMenuItem:disabledItem" data-testid="TreeNode/ContextMenuItem:disabledItem"
disabled={true} disabled={true}
onClick={[Function]} onClick={[Function]}
> >
@@ -1496,7 +1496,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
} }
} }
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1516,14 +1516,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
<MenuPopover> <MenuPopover>
<MenuList> <MenuList>
<MenuItem <MenuItem
data-test="TreeNode/ContextMenuItem:enabledItem" data-testid="TreeNode/ContextMenuItem:enabledItem"
key="enabledItem" key="enabledItem"
onClick={[Function]} onClick={[Function]}
> >
enabledItem enabledItem
</MenuItem> </MenuItem>
<MenuItem <MenuItem
data-test="TreeNode/ContextMenuItem:disabledItem" data-testid="TreeNode/ContextMenuItem:disabledItem"
disabled={true} disabled={true}
key="disabledItem" key="disabledItem"
onClick={[Function]} onClick={[Function]}
@@ -1538,7 +1538,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
exports[`TreeNodeComponent renders a single node 1`] = ` exports[`TreeNodeComponent renders a single node 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1546,7 +1546,7 @@ exports[`TreeNodeComponent renders a single node 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1567,7 +1567,7 @@ exports[`TreeNodeComponent renders a single node 1`] = `
exports[`TreeNodeComponent renders an icon if the node has one 1`] = ` exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1575,7 +1575,7 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""
@@ -1596,7 +1596,7 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
exports[`TreeNodeComponent renders selected parent node as selected if no descendant nodes are selected 1`] = ` exports[`TreeNodeComponent renders selected parent node as selected if no descendant nodes are selected 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1604,10 +1604,10 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl" className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -1626,7 +1626,7 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-test="Tree:root" data-testid="Tree:root"
> >
<TreeNodeComponent <TreeNodeComponent
key="child1Label" key="child1Label"
@@ -1679,7 +1679,7 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
exports[`TreeNodeComponent renders selected parent node as unselected if any descendant node is selected 1`] = ` exports[`TreeNodeComponent renders selected parent node as unselected if any descendant node is selected 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="branch" itemType="branch"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1687,10 +1687,10 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
expandIcon={ expandIcon={
<ChevronRight20Regular <ChevronRight20Regular
data-text="TreeNode/ExpandIcon" data-testid="TreeNode/ExpandIcon"
/> />
} }
iconBefore={ iconBefore={
@@ -1709,7 +1709,7 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n"
data-test="Tree:root" data-testid="Tree:root"
> >
<TreeNodeComponent <TreeNodeComponent
key="child1Label" key="child1Label"
@@ -1763,7 +1763,7 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = ` exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
<TreeItem <TreeItem
className="" className=""
data-test="TreeNodeContainer:root" data-testid="TreeNodeContainer:root"
itemType="leaf" itemType="leaf"
onOpenChange={[Function]} onOpenChange={[Function]}
value="root" value="root"
@@ -1771,7 +1771,7 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl" className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
data-test="TreeNode:root" data-testid="TreeNode:root"
iconBefore={ iconBefore={
<img <img
alt="" alt=""

View File

@@ -38,6 +38,9 @@ import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { UploadDetailsRecord } from "../Contracts/ViewModels"; import { UploadDetailsRecord } from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import MetricScenario from "../Metrics/MetricEvents";
import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig";
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
import { PhoenixClient } from "../Phoenix/PhoenixClient"; import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@@ -402,7 +405,9 @@ export default class Explorer {
updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) => updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) =>
db1.id().localeCompare(db2.id()), db1.id().localeCompare(db2.id()),
); );
useDatabases.setState({ databases: updatedDatabases }); useDatabases.setState({ databases: updatedDatabases, databasesFetchedSuccessfully: true });
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, updatedDatabases); await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, updatedDatabases);
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
@@ -416,6 +421,8 @@ export default class Explorer {
startKey, startKey,
); );
logConsoleError(`Error while refreshing databases: ${errorMessage}`); logConsoleError(`Error while refreshing databases: ${errorMessage}`);
useDatabases.setState({ databasesFetchedSuccessfully: false });
scenarioMonitor.failPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
} }
} }
@@ -1183,6 +1190,11 @@ export default class Explorer {
} }
public async refreshExplorer(): Promise<void> { public async refreshExplorer(): Promise<void> {
// Start DatabaseLoad scenario before fetching databases
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
scenarioMonitor.start(MetricScenario.DatabaseLoad);
}
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()

View File

@@ -76,7 +76,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
name: label, name: label,
disabled: btn.disabled, disabled: btn.disabled,
ariaLabel: btn.ariaLabel, ariaLabel: btn.ariaLabel,
"data-test": `CommandBar/Button:${label}`, "data-testid": `CommandBar/Button:${label}`,
buttonStyles: { buttonStyles: {
root: { root: {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,

View File

@@ -133,7 +133,7 @@ export class NotificationConsoleComponent extends React.Component<
</span> </span>
</span> </span>
</div> </div>
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"> <div className="expandCollapseButton" data-testid="NotificationConsole/ExpandCollapseButton">
<img <img
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon} src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"} alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
@@ -145,7 +145,7 @@ export class NotificationConsoleComponent extends React.Component<
height={this.props.isConsoleExpanded ? "auto" : 0} height={this.props.isConsoleExpanded ? "auto" : 0}
onAnimationEnd={this.onConsoleWasExpanded} onAnimationEnd={this.onConsoleWasExpanded}
> >
<div data-test="NotificationConsole/Contents" className="notificationConsoleContents"> <div data-testid="NotificationConsole/Contents" className="notificationConsoleContents">
<div className="notificationConsoleControls"> <div className="notificationConsoleControls">
<Dropdown <Dropdown
label="Filter:" label="Filter:"

View File

@@ -88,7 +88,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</div> </div>
<div <div
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton" data-testid="NotificationConsole/ExpandCollapseButton"
> >
<img <img
alt="Expand icon" alt="Expand icon"
@@ -122,7 +122,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
> >
<div <div
className="notificationConsoleContents" className="notificationConsoleContents"
data-test="NotificationConsole/Contents" data-testid="NotificationConsole/Contents"
> >
<div <div
className="notificationConsoleControls" className="notificationConsoleControls"
@@ -273,7 +273,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</div> </div>
<div <div
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton" data-testid="NotificationConsole/ExpandCollapseButton"
> >
<img <img
alt="Expand icon" alt="Expand icon"
@@ -307,7 +307,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
> >
<div <div
className="notificationConsoleContents" className="notificationConsoleContents"
data-test="NotificationConsole/Contents" data-testid="NotificationConsole/Contents"
> >
<div <div
className="notificationConsoleControls" className="notificationConsoleControls"

View File

@@ -56,16 +56,16 @@ export class StatusBar extends React.Component<Props> {
return ( return (
<BarContainer> <BarContainer>
<Bar data-test="notebookStatusBar"> <Bar data-testid="notebookStatusBar">
<RightStatus> <RightStatus>
{this.props.lastSaved ? ( {this.props.lastSaved ? (
<p data-test="saveStatus"> Last saved {distanceInWordsToNow(this.props.lastSaved)} </p> <p data-testid="saveStatus"> Last saved {distanceInWordsToNow(this.props.lastSaved)} </p>
) : ( ) : (
<p> Not saved yet </p> <p> Not saved yet </p>
)} )}
</RightStatus> </RightStatus>
<LeftStatus> <LeftStatus>
<p data-test="kernelStatus"> <p data-testid="kernelStatus">
{name} | {this.props.kernelStatus} {name} | {this.props.kernelStatus}
</p> </p>
</LeftStatus> </LeftStatus>

View File

@@ -301,6 +301,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
type="radio" type="radio"
role="radio" role="radio"
id="databaseCreateNew" id="databaseCreateNew"
data-testid="AddCollectionPanel/DatabaseRadio:CreateNew"
tabIndex={0} tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)} onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/> />
@@ -314,6 +315,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
name="databaseType" name="databaseType"
type="radio" type="radio"
role="radio" role="radio"
data-testid="AddCollectionPanel/DatabaseRadio:UseExisting"
tabIndex={0} tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)} onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/> />
@@ -337,6 +339,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
size={40} size={40}
className="panelTextField" className="panelTextField"
aria-label="New database id, Type a new database id" aria-label="New database id, Type a new database id"
data-testid="AddCollectionPanel/DatabaseId"
tabIndex={0} tabIndex={0}
value={this.state.newDatabaseId} value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -346,18 +349,20 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && ( {!isServerlessAccount() && (
<Stack horizontal> <Stack horizontal>
<Checkbox <div data-testid="AddCollectionPanel/SharedThroughputCheckbox">
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`} <Checkbox
checked={this.state.isSharedThroughputChecked} label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
styles={{ checked={this.state.isSharedThroughputChecked}
text: { fontSize: 12 }, styles={{
checkbox: { width: 12, height: 12 }, text: { fontSize: 12 },
label: { padding: 0, alignItems: "center" }, checkbox: { width: 12, height: 12 },
}} label: { padding: 0, alignItems: "center" },
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => }}
this.setState({ isSharedThroughputChecked: isChecked }) onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
} this.setState({ isSharedThroughputChecked: isChecked })
/> }
/>
</div>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName( content={`Throughput configured at the database level will be shared across all ${getCollectionName(
@@ -396,6 +401,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!this.state.createNewDatabase && ( {!this.state.createNewDatabase && (
<Dropdown <Dropdown
ariaLabel="Choose an existing database" ariaLabel="Choose an existing database"
data-testid="AddCollectionPanel/ExistingDatabaseDropdown"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }} styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }} style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database" placeholder="Choose an existing database"
@@ -443,6 +449,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
placeholder={`e.g., ${getCollectionName()}1`} placeholder={`e.g., ${getCollectionName()}1`}
size={40} size={40}
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/CollectionId"
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`} aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
value={this.state.collectionId} value={this.state.collectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -576,6 +583,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
type="text" type="text"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
data-testid="AddCollectionPanel/PartitionKey"
aria-required aria-required
required required
size={40} size={40}
@@ -612,6 +620,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
type="text" type="text"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
data-testid="AddCollectionPanel/PartitionKey"
key={`addCollection-partitionKeyValue_${index}`} key={`addCollection-partitionKeyValue_${index}`}
aria-required aria-required
required required
@@ -729,7 +738,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
{!isFabricNative() && userContext.apiType === "SQL" && ( {!isFabricNative() && userContext.apiType === "SQL" && (
<Stack style={{ marginTop: -2, marginBottom: -4 }}> <Stack style={{ marginTop: -2, marginBottom: -4 }} data-testid="AddCollectionPanel/UniqueKeysSection">
{UniqueKeysHeader()} {UniqueKeysHeader()}
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => { {this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
return ( return (
@@ -743,6 +752,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: "Comma separated paths e.g. /firstName,/address/zipCode" : "Comma separated paths e.g. /firstName,/address/zipCode"
} }
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/UniqueKey"
value={uniqueKey} value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => { const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
@@ -769,6 +779,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<ActionButton <ActionButton
iconProps={{ iconName: "Add" }} iconProps={{ iconName: "Add" }}
data-testid="AddCollectionPanel/AddUniqueKeyButton"
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }} styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })} onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })}
> >

View File

@@ -56,6 +56,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-label="Create new database" aria-label="Create new database"
checked={true} checked={true}
className="panelRadioBtn" className="panelRadioBtn"
data-testid="AddCollectionPanel/DatabaseRadio:CreateNew"
id="databaseCreateNew" id="databaseCreateNew"
name="databaseType" name="databaseType"
onChange={[Function]} onChange={[Function]}
@@ -73,6 +74,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-label="Use existing database" aria-label="Use existing database"
checked={false} checked={false}
className="panelRadioBtn" className="panelRadioBtn"
data-testid="AddCollectionPanel/DatabaseRadio:UseExisting"
name="databaseType" name="databaseType"
onChange={[Function]} onChange={[Function]}
role="radio" role="radio"
@@ -94,6 +96,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-required={true} aria-required={true}
autoComplete="off" autoComplete="off"
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/DatabaseId"
id="newDatabaseId" id="newDatabaseId"
name="newDatabaseId" name="newDatabaseId"
onChange={[Function]} onChange={[Function]}
@@ -109,26 +112,30 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
<Stack <Stack
horizontal={true} horizontal={true}
> >
<StyledCheckboxBase <div
checked={false} data-testid="AddCollectionPanel/SharedThroughputCheckbox"
label="Share throughput across containers" >
onChange={[Function]} <StyledCheckboxBase
styles={ checked={false}
{ label="Share throughput across containers"
"checkbox": { onChange={[Function]}
"height": 12, styles={
"width": 12, {
}, "checkbox": {
"label": { "height": 12,
"alignItems": "center", "width": 12,
"padding": 0, },
}, "label": {
"text": { "alignItems": "center",
"fontSize": 12, "padding": 0,
}, },
"text": {
"fontSize": 12,
},
}
} }
} />
/> </div>
<StyledTooltipHostBase <StyledTooltipHostBase
content="Throughput configured at the database level will be shared across all containers within the database." content="Throughput configured at the database level will be shared across all containers within the database."
directionalHint={4} directionalHint={4}
@@ -191,6 +198,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-required={true} aria-required={true}
autoComplete="off" autoComplete="off"
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/CollectionId"
id="collectionId" id="collectionId"
name="collectionId" name="collectionId"
onChange={[Function]} onChange={[Function]}
@@ -252,6 +260,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-label="Partition key" aria-label="Partition key"
aria-required={true} aria-required={true}
className="panelTextField" className="panelTextField"
data-testid="AddCollectionPanel/PartitionKey"
id="addCollection-partitionKeyValue" id="addCollection-partitionKeyValue"
onChange={[Function]} onChange={[Function]}
pattern=".*" pattern=".*"
@@ -304,6 +313,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
setThroughputValue={[Function]} setThroughputValue={[Function]}
/> />
<Stack <Stack
data-testid="AddCollectionPanel/UniqueKeysSection"
style={ style={
{ {
"marginBottom": -4, "marginBottom": -4,
@@ -338,6 +348,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</StyledTooltipHostBase> </StyledTooltipHostBase>
</Stack> </Stack>
<CustomizedActionButton <CustomizedActionButton
data-testid="AddCollectionPanel/AddUniqueKeyButton"
iconProps={ iconProps={
{ {
"iconName": "Add", "iconName": "Add",

View File

@@ -199,6 +199,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
{keyspaceCreateNew && ( {keyspaceCreateNew && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<TextField <TextField
data-testid="AddCollectionPanel/DatabaseId"
aria-required="true" aria-required="true"
required={true} required={true}
autoComplete="off" autoComplete="off"
@@ -215,16 +216,20 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
{!isServerlessAccount() && ( {!isServerlessAccount() && (
<Stack horizontal> <Stack horizontal>
<Checkbox <div data-testid="AddCollectionPanel/SharedThroughputCheckbox">
label="Provision shared throughput" <Checkbox
checked={isKeyspaceShared} label="Provision shared throughput"
styles={{ checked={isKeyspaceShared}
text: { fontSize: 12 }, styles={{
checkbox: { width: 12, height: 12 }, text: { fontSize: 12 },
label: { padding: 0, alignItems: "center" }, checkbox: { width: 12, height: 12 },
}} label: { padding: 0, alignItems: "center" },
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => setIsKeyspaceShared(isChecked)} }}
/> onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
setIsKeyspaceShared(isChecked)
}
/>
</div>
<InfoTooltip> <InfoTooltip>
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within Provisioned throughput at the keyspace level will be shared across unlimited number of tables within
the keyspace the keyspace
@@ -287,6 +292,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
{`CREATE TABLE ${keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId}.`} {`CREATE TABLE ${keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId}.`}
</Text> </Text>
<TextField <TextField
data-testid="AddCollectionPanel/CollectionId"
underlined underlined
styles={getTextFieldStyles({ fontSize: 12, width: 150 })} styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true" aria-required="true"

View File

@@ -120,6 +120,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text> <Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
<TextField <TextField
id="confirmCollectionId" id="confirmCollectionId"
data-testid="DeleteCollectionConfirmationPane/ConfirmInput"
autoFocus autoFocus
value={inputCollectionName} value={inputCollectionName}
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}

View File

@@ -42,6 +42,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the container id" ariaLabel="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
data-testid="DeleteCollectionConfirmationPane/ConfirmInput"
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
required={true} required={true}
@@ -57,6 +58,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the container id" ariaLabel="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
data-testid="DeleteCollectionConfirmationPane/ConfirmInput"
deferredValidationTime={200} deferredValidationTime={200}
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
@@ -353,6 +355,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
aria-label="Confirm by typing the container id" aria-label="Confirm by typing the container id"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-113" className="ms-TextField-field field-113"
data-testid="DeleteCollectionConfirmationPane/ConfirmInput"
id="confirmCollectionId" id="confirmCollectionId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -379,7 +382,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@@ -390,7 +393,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -681,7 +684,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -977,7 +980,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1271,7 +1274,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2157,7 +2160,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-122" className="ms-Button ms-Button--primary root-122"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-testid="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -135,7 +135,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
<Text variant="small">{confirmDatabase}</Text> <Text variant="small">{confirmDatabase}</Text>
<TextField <TextField
id="confirmDatabaseId" id="confirmDatabaseId"
data-test="Input:confirmDatabaseId" data-testid="DeleteDatabaseConfirmationPanel/ConfirmInput"
autoFocus autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {

View File

@@ -5312,7 +5312,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Execute" ariaLabel="Execute"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Execute" text="Execute"
@@ -5323,7 +5323,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Execute" ariaLabel="Execute"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -5614,7 +5614,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Execute" ariaLabel="Execute"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -5910,7 +5910,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Execute" ariaLabel="Execute"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -6204,7 +6204,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Execute" ariaLabel="Execute"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -7090,7 +7090,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
aria-label="Execute" aria-label="Execute"
className="ms-Button ms-Button--primary root-148" className="ms-Button ms-Button--primary root-148"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-testid="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -54,7 +54,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
return ( return (
<Panel <Panel
data-test={`Panel:${this.props.headerText}`} data-testid={`Panel:${this.props.headerText}`}
headerText={this.props.headerText} headerText={this.props.headerText}
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onDismiss={this.onDissmiss} onDismiss={this.onDissmiss}

View File

@@ -16,7 +16,7 @@ export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
<PrimaryButton <PrimaryButton
type="submit" type="submit"
id="sidePanelOkButton" id="sidePanelOkButton"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
text={buttonLabel} text={buttonLabel}
ariaLabel={buttonLabel} ariaLabel={buttonLabel}
disabled={!!isButtonDisabled} disabled={!!isButtonDisabled}

View File

@@ -21,7 +21,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Load" ariaLabel="Load"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Load" text="Load"
@@ -32,7 +32,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Load" ariaLabel="Load"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -323,7 +323,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Load" ariaLabel="Load"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -619,7 +619,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Load" ariaLabel="Load"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -913,7 +913,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Load" ariaLabel="Load"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1799,7 +1799,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
aria-label="Load" aria-label="Load"
className="ms-Button ms-Button--primary root-109" className="ms-Button ms-Button--primary root-109"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-testid="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -688,7 +688,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Create" ariaLabel="Create"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Create" text="Create"
@@ -699,7 +699,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Create" ariaLabel="Create"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -990,7 +990,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Create" ariaLabel="Create"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1286,7 +1286,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Create" ariaLabel="Create"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1580,7 +1580,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Create" ariaLabel="Create"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2466,7 +2466,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
aria-label="Create" aria-label="Create"
className="ms-Button ms-Button--primary root-128" className="ms-Button ms-Button--primary root-128"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-testid="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -1258,7 +1258,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@@ -1269,7 +1269,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -1560,7 +1560,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1856,7 +1856,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2150,7 +2150,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -3036,7 +3036,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-125" className="ms-Button ms-Button--primary root-125"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-testid="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -369,7 +369,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Add Entity" text="Add Entity"
@@ -380,7 +380,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -671,7 +671,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -967,7 +967,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1261,7 +1261,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Add Entity" ariaLabel="Add Entity"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2147,7 +2147,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
aria-label="Add Entity" aria-label="Add Entity"
className="ms-Button ms-Button--primary root-113" className="ms-Button ms-Button--primary root-113"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-testid="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -375,7 +375,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="Update" ariaLabel="Update"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="Update" text="Update"
@@ -386,7 +386,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="Update" ariaLabel="Update"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -677,7 +677,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="Update" ariaLabel="Update"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -973,7 +973,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="Update" ariaLabel="Update"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1267,7 +1267,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
<BaseButton <BaseButton
ariaLabel="Update" ariaLabel="Update"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2153,7 +2153,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
aria-label="Update" aria-label="Update"
className="ms-Button ms-Button--primary root-113" className="ms-Button ms-Button--primary root-113"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-testid="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -367,7 +367,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the Database id (name)" ariaLabel="Confirm by typing the Database id (name)"
autoFocus={true} autoFocus={true}
data-test="Input:confirmDatabaseId" data-testid="DeleteDatabaseConfirmationPanel/ConfirmInput"
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
required={true} required={true}
@@ -382,7 +382,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the Database id (name)" ariaLabel="Confirm by typing the Database id (name)"
autoFocus={true} autoFocus={true}
data-test="Input:confirmDatabaseId" data-testid="DeleteDatabaseConfirmationPanel/ConfirmInput"
deferredValidationTime={200} deferredValidationTime={200}
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
@@ -678,7 +678,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
aria-label="Confirm by typing the Database id (name)" aria-label="Confirm by typing the Database id (name)"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-117" className="ms-TextField-field field-117"
data-test="Input:confirmDatabaseId" data-testid="DeleteDatabaseConfirmationPanel/ConfirmInput"
id="confirmDatabaseId" id="confirmDatabaseId"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
@@ -1054,7 +1054,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<CustomizedPrimaryButton <CustomizedPrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
text="OK" text="OK"
@@ -1065,7 +1065,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<PrimaryButton <PrimaryButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
styles={ styles={
@@ -1356,7 +1356,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<CustomizedDefaultButton <CustomizedDefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1652,7 +1652,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<DefaultButton <DefaultButton
ariaLabel="OK" ariaLabel="OK"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -1946,7 +1946,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<BaseButton <BaseButton
ariaLabel="OK" ariaLabel="OK"
baseClassName="ms-Button" baseClassName="ms-Button"
data-test="Panel/OkButton" data-testid="Panel/OkButton"
disabled={false} disabled={false}
id="sidePanelOkButton" id="sidePanelOkButton"
onRenderDescription={[Function]} onRenderDescription={[Function]}
@@ -2832,7 +2832,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
aria-label="OK" aria-label="OK"
className="ms-Button ms-Button--primary root-130" className="ms-Button ms-Button--primary root-130"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-testid="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}

View File

@@ -4,7 +4,7 @@ exports[`PaneContainerComponent test should be resize if notification console is
<StyledPanelBase <StyledPanelBase
closeButtonAriaLabel="Close test" closeButtonAriaLabel="Close test"
customWidth="440px" customWidth="440px"
data-test="Panel:test" data-testid="Panel:test"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true} isFooterAtBottom={true}
@@ -43,7 +43,7 @@ exports[`PaneContainerComponent test should not render console with panel 1`] =
<StyledPanelBase <StyledPanelBase
closeButtonAriaLabel="Close test" closeButtonAriaLabel="Close test"
customWidth="440px" customWidth="440px"
data-test="Panel:test" data-testid="Panel:test"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true} isFooterAtBottom={true}
@@ -84,7 +84,7 @@ exports[`PaneContainerComponent test should render with panel content and header
<StyledPanelBase <StyledPanelBase
closeButtonAriaLabel="Close test" closeButtonAriaLabel="Close test"
customWidth="440px" customWidth="440px"
data-test="Panel:test" data-testid="Panel:test"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true} isFooterAtBottom={true}

View File

@@ -242,9 +242,14 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
} }
return ( return (
<div className={styles.globalCommandsContainer} data-test="GlobalCommands"> <div className={styles.globalCommandsContainer} data-testid="GlobalCommands">
{actions.length === 1 ? ( {actions.length === 1 ? (
<Button icon={primaryAction.icon} onClick={onPrimaryActionClick} ref={primaryFocusableRef}> <Button
data-testid={`GlobalCommands/Button:${primaryAction.label}`}
icon={primaryAction.icon}
onClick={onPrimaryActionClick}
ref={primaryFocusableRef}
>
{primaryAction.label} {primaryAction.label}
</Button> </Button>
) : ( ) : (
@@ -253,8 +258,12 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
{(triggerProps: MenuButtonProps) => ( {(triggerProps: MenuButtonProps) => (
<div ref={setGlobalCommandButton}> <div ref={setGlobalCommandButton}>
<SplitButton <SplitButton
data-testid={`GlobalCommands/Button:${primaryAction.label}`}
menuButton={{ ...triggerProps, "aria-label": "More commands" }} menuButton={{ ...triggerProps, "aria-label": "More commands" }}
primaryActionButton={{ onClick: onPrimaryActionClick, ref: primaryFocusableRef }} primaryActionButton={{
onClick: onPrimaryActionClick,
ref: primaryFocusableRef,
}}
className={styles.globalCommandsSplitButton} className={styles.globalCommandsSplitButton}
icon={primaryAction.icon} icon={primaryAction.icon}
> >
@@ -376,7 +385,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
{!isFabricNative() && ( {!isFabricNative() && (
<button <button
type="button" type="button"
data-test="Sidebar/RefreshButton" data-testid="Sidebar/RefreshButton"
className={styles.floatingControlButton} className={styles.floatingControlButton}
disabled={loading} disabled={loading}
title="Refresh" title="Refresh"

View File

@@ -35,6 +35,15 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
jest.mock("rx-jupyter", () => ({
sessions: {
create: jest.fn(),
},
contents: {
JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
},
}));
jest.mock("Common/dataAccess/queryDocuments", () => ({ jest.mock("Common/dataAccess/queryDocuments", () => ({
queryDocuments: jest.fn(() => ({ queryDocuments: jest.fn(() => ({
// Omit headers, because we can't mock a private field and we don't need to test it // Omit headers, because we can't mock a private field and we don't need to test it

View File

@@ -2146,8 +2146,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return ( return (
<CosmosFluentProvider className={styles.container}> <CosmosFluentProvider className={styles.container}>
<div data-test={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}> <div data-testid={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
<div data-test={"DocumentsTab/Filter"} className={`${styles.filterRow} ${styles.smallScreenContent}`}> <div data-testid={"DocumentsTab/Filter"} className={`${styles.filterRow} ${styles.smallScreenContent}`}>
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>} {!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
<InputDataList <InputDataList
dropdownOptions={getFilterChoices()} dropdownOptions={getFilterChoices()}
@@ -2164,7 +2164,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
/> />
<Button <Button
appearance="primary" appearance="primary"
data-test={"DocumentsTab/ApplyFilter"} data-testid={"DocumentsTab/ApplyFilter"}
size="small" size="small"
onClick={() => { onClick={() => {
if (isExecuting) { if (isExecuting) {
@@ -2191,7 +2191,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
> >
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}> <Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
<div <div
data-test={"DocumentsTab/DocumentsPane"} data-testid={"DocumentsTab/DocumentsPane"}
style={{ height: "100%", width: "100%", overflow: "hidden" }} style={{ height: "100%", width: "100%", overflow: "hidden" }}
ref={tableContainerRef} ref={tableContainerRef}
> >
@@ -2237,7 +2237,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
{tableItems.length > 0 && ( {tableItems.length > 0 && (
<a <a
className={styles.loadMore} className={styles.loadMore}
data-test={"DocumentsTab/LoadMore"} data-testid={"DocumentsTab/LoadMore"}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => loadNextPage(documentsIterator.iterator, false)} onClick={() => loadNextPage(documentsIterator.iterator, false)}
@@ -2249,7 +2249,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
</div> </div>
</Allotment.Pane> </Allotment.Pane>
<Allotment.Pane minSize={30}> <Allotment.Pane minSize={30}>
<div data-test={"DocumentsTab/ResultsPane"} style={{ height: "100%", width: "100%" }}> <div data-testid={"DocumentsTab/ResultsPane"} style={{ height: "100%", width: "100%" }}>
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && ( {isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
<EditorReact <EditorReact
language={"json"} language={"json"}

View File

@@ -19,6 +19,15 @@ import { act } from "react-dom/test-utils";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
jest.mock("rx-jupyter", () => ({
sessions: {
create: jest.fn(),
},
contents: {
JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
},
}));
jest.requireActual("Explorer/Controls/Editor/EditorReact"); jest.requireActual("Explorer/Controls/Editor/EditorReact");
const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__"; const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__";

View File

@@ -6,7 +6,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
> >
<div <div
className="tab-pane active" className="tab-pane active"
data-test="DocumentsTab" data-testid="DocumentsTab"
role="tabpanel" role="tabpanel"
style={ style={
{ {
@@ -16,7 +16,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
> >
<div <div
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29 ___1ngl8o6_0000000 fz7mnu6 fl3egqs flhmrkm" className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29 ___1ngl8o6_0000000 fz7mnu6 fl3egqs flhmrkm"
data-test="DocumentsTab/Filter" data-testid="DocumentsTab/Filter"
> >
<span> <span>
SELECT * FROM c SELECT * FROM c
@@ -51,7 +51,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
<Button <Button
appearance="primary" appearance="primary"
aria-label="Apply filter" aria-label="Apply filter"
data-test="DocumentsTab/ApplyFilter" data-testid="DocumentsTab/ApplyFilter"
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
size="small" size="small"
@@ -68,7 +68,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
preferredSize="35%" preferredSize="35%"
> >
<div <div
data-test="DocumentsTab/DocumentsPane" data-testid="DocumentsTab/DocumentsPane"
style={ style={
{ {
"height": "100%", "height": "100%",
@@ -130,7 +130,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
minSize={30} minSize={30}
> >
<div <div
data-test="DocumentsTab/ResultsPane" data-testid="DocumentsTab/ResultsPane"
style={ style={
{ {
"height": "100%", "height": "100%",

View File

@@ -116,7 +116,7 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
return ( return (
<DataGrid <DataGrid
data-test="QueryTab/ResultsPane/ErrorList" data-testid="QueryTab/ResultsPane/ErrorList"
items={errors} items={errors}
columns={columns} columns={columns}
sortable sortable
@@ -131,9 +131,9 @@ export const ErrorList: React.FC<{ errors: QueryError[] }> = ({ errors }) => {
</DataGridHeader> </DataGridHeader>
<DataGridBody<QueryError>> <DataGridBody<QueryError>>
{({ item, rowId }) => ( {({ item, rowId }) => (
<DataGridRow<QueryError> key={rowId} data-test={`Row:${rowId}`}> <DataGridRow<QueryError> key={rowId} data-testid={`Row:${rowId}`}>
{({ columnId, renderCell }) => ( {({ columnId, renderCell }) => (
<DataGridCell data-test={`Row:${rowId}/Column:${columnId}`}>{renderCell(item)}</DataGridCell> <DataGridCell data-testid={`Row:${rowId}/Column:${columnId}`}>{renderCell(item)}</DataGridCell>
)} )}
</DataGridRow> </DataGridRow>
)} )}

View File

@@ -3,13 +3,13 @@ import QueryError from "Common/QueryError";
import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar"; import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar";
import { MessageBanner } from "Explorer/Controls/MessageBanner"; import { MessageBanner } from "Explorer/Controls/MessageBanner";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles"; import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import useZoomLevel from "hooks/useZoomLevel";
import React from "react"; import React from "react";
import { conditionalClass } from "Utils/StyleUtils";
import RunQuery from "../../../../images/RunQuery.png"; import RunQuery from "../../../../images/RunQuery.png";
import { QueryResults } from "../../../Contracts/ViewModels"; import { QueryResults } from "../../../Contracts/ViewModels";
import { ErrorList } from "./ErrorList"; import { ErrorList } from "./ErrorList";
import { ResultsView } from "./ResultsView"; import { ResultsView } from "./ResultsView";
import useZoomLevel from "hooks/useZoomLevel";
import { conditionalClass } from "Utils/StyleUtils";
export interface ResultsViewProps { export interface ResultsViewProps {
isMongoDB: boolean; isMongoDB: boolean;
@@ -27,7 +27,7 @@ const ExecuteQueryCallToAction: React.FC = () => {
const styles = useQueryTabStyles(); const styles = useQueryTabStyles();
const isZoomed = useZoomLevel(); const isZoomed = useZoomLevel();
return ( return (
<div data-test="QueryTab/ResultsPane/ExecuteCTA" className={styles.executeCallToAction}> <div data-testid="QueryTab/ResultsPane/ExecuteCTA" className={styles.executeCallToAction}>
<div> <div>
<p> <p>
<img <img
@@ -54,7 +54,7 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent); const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
return ( return (
<div data-test="QueryTab/ResultsPane" className={styles.queryResultsPanel}> <div data-testid="QueryTab/ResultsPane" className={styles.queryResultsPanel}>
{isExecuting && <IndeterminateProgressBar />} {isExecuting && <IndeterminateProgressBar />}
<MessageBanner <MessageBanner
messageId="QueryEditor.EmptyMongoQuery" messageId="QueryEditor.EmptyMongoQuery"

View File

@@ -64,7 +64,7 @@ describe("QueryTabComponent", () => {
const { container } = render(<QueryTabComponent {...propsMock} />); const { container } = render(<QueryTabComponent {...propsMock} />);
const launchCopilotButton = container.querySelector('[data-test="QueryTab/ResultsPane/ExecuteCTA"]'); const launchCopilotButton = container.querySelector('[data-testid="QueryTab/ResultsPane/ExecuteCTA"]');
fireEvent.keyDown(launchCopilotButton, { key: "c", altKey: true }); fireEvent.keyDown(launchCopilotButton, { key: "c", altKey: true });
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true); expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);

View File

@@ -746,7 +746,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}} }}
> >
<Allotment.Pane <Allotment.Pane
data-test="QueryTab/EditorPane" data-testid="QueryTab/EditorPane"
preferredSize={ preferredSize={
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
} }
@@ -813,7 +813,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
render(): JSX.Element { render(): JSX.Element {
const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive; const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive;
return ( return (
<div data-test="QueryTab" style={{ display: "flex", flexDirection: "row", height: "100%" }}> <div data-testid="QueryTab" style={{ display: "flex", flexDirection: "row", height: "100%" }}>
<div style={{ width: shouldScaleElements ? "70%" : "100%", height: "100%" }}> <div style={{ width: shouldScaleElements ? "70%" : "100%", height: "100%" }}>
{this.getEditorAndQueryResult()} {this.getEditorAndQueryResult()}
</div> </div>

View File

@@ -489,7 +489,7 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
return ( return (
<div className={styles.metricsGridContainer}> <div className={styles.metricsGridContainer}>
<DataGrid <DataGrid
data-test="QueryTab/ResultsPane/ResultsView/QueryStatsList" data-testid="QueryTab/ResultsPane/ResultsView/QueryStatsList"
className={styles.queryStatsGrid} className={styles.queryStatsGrid}
items={generateQueryStatsItems()} items={generateQueryStatsItems()}
columns={columns} columns={columns}
@@ -504,9 +504,9 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
</DataGridHeader> </DataGridHeader>
<DataGridBody<IDocument>> <DataGridBody<IDocument>>
{({ item, rowId }) => ( {({ item, rowId }) => (
<DataGridRow<IDocument> key={rowId} data-test={`Row:${rowId}`}> <DataGridRow<IDocument> key={rowId} data-testid={`Row:${rowId}`}>
{({ columnId, renderCell }) => ( {({ columnId, renderCell }) => (
<DataGridCell data-test={`Row:${rowId}/Column:${columnId}`}>{renderCell(item)}</DataGridCell> <DataGridCell data-testid={`Row:${rowId}/Column:${columnId}`}>{renderCell(item)}</DataGridCell>
)} )}
</DataGridRow> </DataGridRow>
)} )}
@@ -532,17 +532,17 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
}, []); }, []);
return ( return (
<div data-test="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}> <div data-testid="QueryTab/ResultsPane/ResultsView" className={styles.queryResultsTabPanel}>
<TabList selectedValue={activeTab} onTabSelect={onTabSelect}> <TabList selectedValue={activeTab} onTabSelect={onTabSelect}>
<Tab <Tab
data-test="QueryTab/ResultsPane/ResultsView/ResultsTab" data-testid="QueryTab/ResultsPane/ResultsView/ResultsTab"
id={ResultsTabs.Results} id={ResultsTabs.Results}
value={ResultsTabs.Results} value={ResultsTabs.Results}
> >
Results Results
</Tab> </Tab>
<Tab <Tab
data-test="QueryTab/ResultsPane/ResultsView/QueryStatsTab" data-testid="QueryTab/ResultsPane/ResultsView/QueryStatsTab"
id={ResultsTabs.QueryStats} id={ResultsTabs.QueryStats}
value={ResultsTabs.QueryStats} value={ResultsTabs.QueryStats}
> >

View File

@@ -237,14 +237,14 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
if (tab) { if (tab) {
if ("render" in tab) { if ("render" in tab) {
return ( return (
<div data-test={`Tab:${tab.tabId}`} {...attrs}> <div data-testid={`Tab:${tab.tabId}`} {...attrs}>
{tab.render()} {tab.render()}
</div> </div>
); );
} }
} }
return <div data-test={`Tab:${tab.tabId}`} {...attrs} ref={ref} data-bind="html:html" />; return <div data-testid={`Tab:${tab.tabId}`} {...attrs} ref={ref} data-bind="html:html" />;
} }
const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => { const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => {

View File

@@ -6,6 +6,15 @@ import { updateUserContext, userContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import Database from "./Database"; import Database from "./Database";
jest.mock("rx-jupyter", () => ({
sessions: {
create: jest.fn(),
},
contents: {
JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
},
}));
const createMockContainer = (): Explorer => { const createMockContainer = (): Explorer => {
const mockContainer = new Explorer(); const mockContainer = new Explorer();
return mockContainer; return mockContainer;

View File

@@ -17,6 +17,7 @@ import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
import { useDatabaseLoadScenario } from "../../Metrics/useMetricPhases";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
@@ -53,6 +54,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
resourceTokenCollection: state.resourceTokenCollection, resourceTokenCollection: state.resourceTokenCollection,
sampleDataResourceTokenCollection: state.sampleDataResourceTokenCollection, sampleDataResourceTokenCollection: state.sampleDataResourceTokenCollection,
})); }));
const databasesFetchedSuccessfully = useDatabases((state) => state.databasesFetchedSuccessfully);
const { isCopilotEnabled, isCopilotSampleDBEnabled } = useQueryCopilot((state) => ({ const { isCopilotEnabled, isCopilotSampleDBEnabled } = useQueryCopilot((state) => ({
isCopilotEnabled: state.copilotEnabled, isCopilotEnabled: state.copilotEnabled,
isCopilotSampleDBEnabled: state.copilotSampleDBEnabled, isCopilotSampleDBEnabled: state.copilotSampleDBEnabled,
@@ -114,6 +116,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
} }
}, [databaseTreeNodes, sampleDataNodes]); }, [databaseTreeNodes, sampleDataNodes]);
// Track complete DatabaseLoad scenario (start, tree rendered, interactive)
useDatabaseLoadScenario(databaseTreeNodes, databasesFetchedSuccessfully);
useEffect(() => { useEffect(() => {
// Compute open items based on node.isExpanded // Compute open items based on node.isExpanded
const updateOpenItems = (node: TreeNode, parentNodeId: string): void => { const updateOpenItems = (node: TreeNode, parentNodeId: string): void => {

View File

@@ -9,6 +9,7 @@ interface DatabasesState {
databases: ViewModels.Database[]; databases: ViewModels.Database[];
resourceTokenCollection: ViewModels.CollectionBase; resourceTokenCollection: ViewModels.CollectionBase;
sampleDataResourceTokenCollection: ViewModels.CollectionBase; sampleDataResourceTokenCollection: ViewModels.CollectionBase;
databasesFetchedSuccessfully: boolean; // Track if last database fetch was successful
updateDatabase: (database: ViewModels.Database) => void; updateDatabase: (database: ViewModels.Database) => void;
addDatabases: (databases: ViewModels.Database[]) => void; addDatabases: (databases: ViewModels.Database[]) => void;
deleteDatabase: (database: ViewModels.Database) => void; deleteDatabase: (database: ViewModels.Database) => void;
@@ -30,6 +31,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
databases: [], databases: [],
resourceTokenCollection: undefined, resourceTokenCollection: undefined,
sampleDataResourceTokenCollection: undefined, sampleDataResourceTokenCollection: undefined,
databasesFetchedSuccessfully: false,
updateDatabase: (updatedDatabase: ViewModels.Database) => updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => { set((state) => {
const updatedDatabases = state.databases.map((database: ViewModels.Database) => { const updatedDatabases = state.databases.map((database: ViewModels.Database) => {

View File

@@ -129,7 +129,7 @@ const App: React.FunctionComponent = () => {
// Setting key is needed so React will re-render this element on any account change // Setting key is needed so React will re-render this element on any account change
key={databaseAccount?.id || encryptedTokenMetadata?.accountName || authType} key={databaseAccount?.id || encryptedTokenMetadata?.accountName || authType}
ref={ref} ref={ref}
data-test="DataExplorerFrame" data-testid="DataExplorerFrame"
id="explorerMenu" id="explorerMenu"
name="explorer" name="explorer"
className="iframe" className="iframe"

View File

@@ -60,6 +60,10 @@ import "./Explorer/Panes/PanelComponent.less";
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent"; import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
import "./Explorer/SplashScreen/SplashScreen.less"; import "./Explorer/SplashScreen/SplashScreen.less";
import "./Libs/jquery"; import "./Libs/jquery";
import MetricScenario from "./Metrics/MetricEvents";
import { MetricScenarioProvider, useMetricScenario } from "./Metrics/MetricScenarioProvider";
import { ApplicationMetricPhase } from "./Metrics/ScenarioConfig";
import { useInteractive } from "./Metrics/useMetricPhases";
import { appThemeFabric } from "./Platform/Fabric/FabricTheme"; import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
@@ -79,13 +83,27 @@ const App: React.FunctionComponent = () => {
StyleConstants.updateStyles(); StyleConstants.updateStyles();
const explorer = useKnockoutExplorer(config?.platform); const explorer = useKnockoutExplorer(config?.platform);
// Scenario-based health tracking: start ApplicationLoad and complete phases.
const { startScenario, completePhase } = useMetricScenario();
React.useEffect(() => {
startScenario(MetricScenario.ApplicationLoad);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (explorer) {
completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [explorer]);
if (!explorer) { if (!explorer) {
return <LoadingExplorer />; return <LoadingExplorer />;
} }
return ( return (
<KeyboardShortcutRoot> <KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false" data-test="DataExplorerRoot"> <div className="flexContainer" aria-hidden="false" data-testid="DataExplorerRoot">
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? ( {userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
<ContainerCopyPanel explorer={explorer} /> <ContainerCopyPanel explorer={explorer} />
) : ( ) : (
@@ -104,9 +122,16 @@ const App: React.FunctionComponent = () => {
}; };
const mainElement = document.getElementById("Main"); const mainElement = document.getElementById("Main");
ReactDOM.render(<App />, mainElement); ReactDOM.render(
<MetricScenarioProvider>
<App />
</MetricScenarioProvider>,
mainElement,
);
function DivExplorer({ explorer }: { explorer: Explorer }): JSX.Element { function DivExplorer({ explorer }: { explorer: Explorer }): JSX.Element {
useInteractive(MetricScenario.ApplicationLoad);
return ( return (
<div id="divExplorer" className="flexContainer hideOverflows"> <div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div> <div id="freeTierTeachingBubble"> </div>

16
src/Metrics/Constants.ts Normal file
View File

@@ -0,0 +1,16 @@
import { ApiType } from "Common/Constants";
import { Platform } from "ConfigContext";
// Metric scenarios represent lifecycle checkpoints we measure.
export enum MetricScenario {
ApplicationLoad = "ApplicationLoad",
DatabaseLoad = "DatabaseLoad",
}
// Generic metric emission event describing scenario outcome.
export interface MetricEvent {
readonly platform: Platform;
readonly api: ApiType;
readonly scenario: MetricScenario;
readonly healthy: boolean;
}

View File

@@ -0,0 +1,104 @@
import { configContext, Platform } from "../ConfigContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { fetchWithTimeout } from "../Utils/FetchWithTimeout";
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Response } = require("node-fetch");
jest.mock("../Utils/AuthorizationUtils", () => ({
getAuthorizationHeader: jest.fn().mockReturnValue({ header: "authorization", token: "Bearer test-token" }),
}));
jest.mock("../Utils/FetchWithTimeout", () => ({
fetchWithTimeout: jest.fn(),
}));
describe("MetricEvents", () => {
const mockFetchWithTimeout = fetchWithTimeout as jest.MockedFunction<typeof fetchWithTimeout>;
afterEach(() => {
jest.clearAllMocks();
});
test("reportHealthy success includes auth header", async () => {
const mockResponse = new Response(null, { status: 200 });
mockFetchWithTimeout.mockResolvedValue(mockResponse);
const result = await reportHealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL");
expect(result).toBeInstanceOf(Response);
expect(result.ok).toBe(true);
expect(result.status).toBe(200);
expect(mockFetchWithTimeout).toHaveBeenCalledTimes(1);
const callArgs = mockFetchWithTimeout.mock.calls[0];
expect(callArgs[0]).toContain("/api/dataexplorer/metrics/health");
expect(callArgs[1]?.headers).toEqual({
"Content-Type": "application/json",
authorization: "Bearer test-token",
});
const body = JSON.parse(callArgs[1]?.body as string);
expect(body.scenario).toBe(MetricScenario.ApplicationLoad);
expect(body.platform).toBe(Platform.Portal);
expect(body.api).toBe("SQL");
expect(body.healthy).toBe(true);
expect(getAuthorizationHeader).toHaveBeenCalled();
});
test("reportUnhealthy failure status", async () => {
const mockResponse = new Response("Failure", { status: 500 });
mockFetchWithTimeout.mockResolvedValue(mockResponse);
const result = await reportUnhealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL");
expect(result).toBeInstanceOf(Response);
expect(result.ok).toBe(false);
expect(result.status).toBe(500);
const callArgs = mockFetchWithTimeout.mock.calls[0];
const body = JSON.parse(callArgs[1]?.body as string);
expect(body.healthy).toBe(false);
});
test("helpers healthy/unhealthy", async () => {
mockFetchWithTimeout.mockResolvedValue(new Response(null, { status: 201 }));
const healthyResult = await reportHealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL");
const unhealthyResult = await reportUnhealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL");
expect(healthyResult.status).toBe(201);
expect(unhealthyResult.status).toBe(201);
expect(mockFetchWithTimeout).toHaveBeenCalledTimes(2);
});
test("throws when backend endpoint missing", async () => {
const original = configContext.PORTAL_BACKEND_ENDPOINT;
(configContext as { PORTAL_BACKEND_ENDPOINT: string }).PORTAL_BACKEND_ENDPOINT = "";
await expect(reportHealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL")).rejects.toThrow(
"baseUri is null or empty",
);
expect(mockFetchWithTimeout).not.toHaveBeenCalled();
(configContext as { PORTAL_BACKEND_ENDPOINT: string }).PORTAL_BACKEND_ENDPOINT = original;
});
test("propagates fetch errors", async () => {
mockFetchWithTimeout.mockRejectedValue(new Error("Network error"));
await expect(reportHealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL")).rejects.toThrow(
"Network error",
);
});
test("propagates timeout errors", async () => {
const abortError = new DOMException("The operation was aborted", "AbortError");
mockFetchWithTimeout.mockRejectedValue(abortError);
await expect(reportUnhealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL")).rejects.toThrow(
"The operation was aborted",
);
});
});

View File

@@ -0,0 +1,28 @@
// Metrics module: scenario metric emission logic.
import { MetricEvent, MetricScenario } from "Metrics/Constants";
import { createUri } from "../Common/UrlUtility";
import { configContext, Platform } from "../ConfigContext";
import { ApiType } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { fetchWithTimeout } from "../Utils/FetchWithTimeout";
const RELATIVE_PATH = "/api/dataexplorer/metrics/health"; // Endpoint retains 'health' for backend compatibility.
export const reportHealthy = (scenario: MetricScenario, platform: Platform, api: ApiType): Promise<Response> =>
send({ platform, api, scenario, healthy: true });
export const reportUnhealthy = (scenario: MetricScenario, platform: Platform, api: ApiType): Promise<Response> =>
send({ platform, api, scenario, healthy: false });
const send = async (event: MetricEvent): Promise<Response> => {
const url = createUri(configContext?.PORTAL_BACKEND_ENDPOINT, RELATIVE_PATH);
const authHeader = getAuthorizationHeader();
return await fetchWithTimeout(url, {
method: "POST",
headers: { "Content-Type": "application/json", [authHeader.header]: authHeader.token },
body: JSON.stringify(event),
});
};
export default MetricScenario;

View File

@@ -0,0 +1,17 @@
import MetricScenario from "./MetricEvents";
import { ApplicationMetricPhase, CommonMetricPhase, ScenarioConfig } from "./ScenarioConfig";
export const scenarioConfigs: Record<MetricScenario, ScenarioConfig> = {
[MetricScenario.ApplicationLoad]: {
requiredPhases: [ApplicationMetricPhase.ExplorerInitialized, CommonMetricPhase.Interactive],
timeoutMs: 10000,
},
[MetricScenario.DatabaseLoad]: {
requiredPhases: [
ApplicationMetricPhase.DatabasesFetched,
ApplicationMetricPhase.DatabaseTreeRendered,
CommonMetricPhase.Interactive,
],
timeoutMs: 10000,
},
};

View File

@@ -0,0 +1,29 @@
import React, { useContext } from "react";
import MetricScenario from "./MetricEvents";
import { MetricPhase } from "./ScenarioConfig";
import { scenarioMonitor } from "./ScenarioMonitor";
interface MetricScenarioContextValue {
startScenario: (scenario: MetricScenario) => void;
startPhase: (scenario: MetricScenario, phase: MetricPhase) => void;
completePhase: (scenario: MetricScenario, phase: MetricPhase) => void;
}
const MetricScenarioContext = React.createContext<MetricScenarioContextValue | undefined>(undefined);
export const MetricScenarioProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const value: MetricScenarioContextValue = {
startScenario: (s: MetricScenario) => scenarioMonitor.start(s),
startPhase: (s: MetricScenario, p: MetricPhase) => scenarioMonitor.startPhase(s, p),
completePhase: (s: MetricScenario, p: MetricPhase) => scenarioMonitor.completePhase(s, p),
};
return <MetricScenarioContext.Provider value={value}>{children}</MetricScenarioContext.Provider>;
};
export function useMetricScenario(): MetricScenarioContextValue {
const ctx = useContext(MetricScenarioContext);
if (!ctx) {
throw new Error("useMetricScenario must be used within MetricScenarioProvider");
}
return ctx;
}

View File

@@ -0,0 +1,47 @@
import MetricScenario from "./MetricEvents";
// Common phases shared across all scenarios
export enum CommonMetricPhase {
Interactive = "Interactive",
}
// Application-specific phases
export enum ApplicationMetricPhase {
ExplorerInitialized = "ExplorerInitialized",
DatabasesFetched = "DatabasesFetched",
DatabaseTreeRendered = "DatabaseTreeRendered",
}
// Combined type for all metric phases
export type MetricPhase = CommonMetricPhase | ApplicationMetricPhase;
export interface WebVitals {
lcp?: number; // Largest Contentful Paint
inp?: number; // Interaction to Next Paint
cls?: number; // Cumulative Layout Shift
fcp?: number; // First Contentful Paint
ttfb?: number; // Time to First Byte
}
export interface ScenarioConfig<TPhase extends string = MetricPhase> {
requiredPhases: TPhase[];
timeoutMs: number;
validate?: (ctx: ScenarioContextSnapshot<TPhase>) => boolean; // Optional custom validation
}
export interface PhaseTimings {
endTimeISO: string; // When the phase completed
durationMs: number; // Duration from scenario start to phase completion
}
export interface ScenarioContextSnapshot<TPhase extends string = MetricPhase> {
scenario: MetricScenario;
startTimeISO: string; // Human-readable ISO timestamp
endTimeISO: string; // Human-readable end timestamp
durationMs: number; // Total scenario duration from start to end
completed: TPhase[]; // Array for JSON serialization
failedPhases?: TPhase[]; // Phases that failed
timedOut: boolean;
vitals?: WebVitals;
phaseTimings?: Record<string, PhaseTimings>; // Start/end times for each phase
}

View File

@@ -0,0 +1,267 @@
import { Metric, onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
import { configContext } from "../ConfigContext";
import { trackEvent } from "../Shared/appInsights";
import { userContext } from "../UserContext";
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
import { scenarioConfigs } from "./MetricScenarioConfigs";
import { MetricPhase, PhaseTimings, ScenarioConfig, ScenarioContextSnapshot, WebVitals } from "./ScenarioConfig";
interface PhaseContext {
startMarkName: string; // Performance mark name for phase start
endMarkName?: string; // Performance mark name for phase end
}
interface InternalScenarioContext {
scenario: MetricScenario;
config: ScenarioConfig;
startMarkName: string;
completed: Set<MetricPhase>;
failed: Set<MetricPhase>;
phases: Map<MetricPhase, PhaseContext>; // Track start/end for each phase
timeoutId?: number;
emitted: boolean;
}
class ScenarioMonitor {
private contexts = new Map<MetricScenario, InternalScenarioContext>();
private vitals: WebVitals = {};
private vitalsInitialized = false;
constructor() {
this.initializeVitals();
}
private initializeVitals() {
if (this.vitalsInitialized) {
return;
}
this.vitalsInitialized = true;
onLCP((metric: Metric) => {
this.vitals.lcp = metric.value;
});
onINP((metric: Metric) => {
this.vitals.inp = metric.value;
});
onCLS((metric: Metric) => {
this.vitals.cls = metric.value;
});
onFCP((metric: Metric) => {
this.vitals.fcp = metric.value;
});
onTTFB((metric: Metric) => {
this.vitals.ttfb = metric.value;
});
}
start(scenario: MetricScenario) {
if (this.contexts.has(scenario)) {
return;
}
const config = scenarioConfigs[scenario];
if (!config) {
throw new Error(`Missing scenario config for ${scenario}`);
}
const startMarkName = `scenario_${scenario}_start`;
performance.mark(startMarkName);
const ctx: InternalScenarioContext = {
scenario,
config,
startMarkName,
completed: new Set<MetricPhase>(),
failed: new Set<MetricPhase>(),
phases: new Map<MetricPhase, PhaseContext>(),
emitted: false,
};
// Start all required phases at scenario start time
config.requiredPhases.forEach((phase) => {
const phaseStartMarkName = `scenario_${scenario}_${phase}_start`;
performance.mark(phaseStartMarkName);
ctx.phases.set(phase, { startMarkName: phaseStartMarkName });
});
ctx.timeoutId = window.setTimeout(() => this.emit(ctx, false, true), config.timeoutMs);
this.contexts.set(scenario, ctx);
}
startPhase(scenario: MetricScenario, phase: MetricPhase) {
const ctx = this.contexts.get(scenario);
if (!ctx || ctx.emitted || !ctx.config.requiredPhases.includes(phase) || ctx.phases.has(phase)) {
return;
}
const startMarkName = `scenario_${scenario}_${phase}_start`;
performance.mark(startMarkName);
ctx.phases.set(phase, { startMarkName });
}
completePhase(scenario: MetricScenario, phase: MetricPhase) {
const ctx = this.contexts.get(scenario);
const phaseCtx = ctx?.phases.get(phase);
if (!ctx || ctx.emitted || !ctx.config.requiredPhases.includes(phase) || !phaseCtx) {
return;
}
const endMarkName = `scenario_${scenario}_${phase}_end`;
performance.mark(endMarkName);
phaseCtx.endMarkName = endMarkName;
ctx.completed.add(phase);
this.tryEmitIfReady(ctx);
}
failPhase(scenario: MetricScenario, phase: MetricPhase) {
const ctx = this.contexts.get(scenario);
if (!ctx || ctx.emitted) {
return;
}
// Mark the explicitly failed phase
performance.mark(`scenario_${scenario}_${phase}_failed`);
ctx.failed.add(phase);
// Mark all remaining incomplete required phases as failed
ctx.config.requiredPhases.forEach((requiredPhase) => {
if (!ctx.completed.has(requiredPhase) && !ctx.failed.has(requiredPhase)) {
ctx.failed.add(requiredPhase);
}
});
// Build a snapshot with failure info
const failureSnapshot = this.buildSnapshot(ctx, { final: false, timedOut: false });
// Emit unhealthy immediately
this.emit(ctx, false, false, failureSnapshot);
}
private tryEmitIfReady(ctx: InternalScenarioContext) {
const allDone = ctx.config.requiredPhases.every((p) => ctx.completed.has(p));
if (!allDone) {
return;
}
const finalSnapshot = this.buildSnapshot(ctx, { final: true, timedOut: false });
const healthy = ctx.config.validate ? ctx.config.validate(finalSnapshot) : true;
this.emit(ctx, healthy, false, finalSnapshot);
}
private getPhaseTimings(ctx: InternalScenarioContext): Record<string, PhaseTimings> {
const result: Record<string, PhaseTimings> = {};
const navigationStart = performance.timeOrigin;
ctx.phases.forEach((phaseCtx, phase) => {
// Only include completed phases (those with endMarkName)
if (phaseCtx.endMarkName) {
const endEntry = performance.getEntriesByName(phaseCtx.endMarkName)[0];
if (endEntry) {
const endTimeISO = new Date(navigationStart + endEntry.startTime).toISOString();
// Use Performance API measure to calculate duration
const measureName = `scenario_${ctx.scenario}_${phase}_duration`;
performance.measure(measureName, phaseCtx.startMarkName, phaseCtx.endMarkName);
const measure = performance.getEntriesByName(measureName)[0];
if (measure) {
result[phase] = {
endTimeISO,
durationMs: measure.duration,
};
}
}
}
});
return result;
}
private emit(ctx: InternalScenarioContext, healthy: boolean, timedOut: boolean, snapshot?: ScenarioContextSnapshot) {
if (ctx.emitted) {
return;
}
ctx.emitted = true;
if (ctx.timeoutId) {
clearTimeout(ctx.timeoutId);
ctx.timeoutId = undefined;
}
const platform = configContext.platform;
const api = userContext.apiType;
// Build snapshot if not provided
const finalSnapshot = snapshot || this.buildSnapshot(ctx, { final: false, timedOut });
// Emit enriched telemetry with performance data
// TODO: Call portal backend metrics endpoint
trackEvent(
{ name: "MetricScenarioComplete" },
{
scenario: ctx.scenario,
healthy: healthy.toString(),
timedOut: timedOut.toString(),
platform,
api,
durationMs: finalSnapshot.durationMs.toString(),
completedPhases: finalSnapshot.completed.join(","),
failedPhases: finalSnapshot.failedPhases?.join(","),
lcp: finalSnapshot.vitals?.lcp?.toString(),
inp: finalSnapshot.vitals?.inp?.toString(),
cls: finalSnapshot.vitals?.cls?.toString(),
fcp: finalSnapshot.vitals?.fcp?.toString(),
ttfb: finalSnapshot.vitals?.ttfb?.toString(),
phaseTimings: JSON.stringify(finalSnapshot.phaseTimings),
},
);
// Call portal backend health metrics endpoint
if (healthy && !timedOut) {
reportHealthy(ctx.scenario, platform, api);
} else {
reportUnhealthy(ctx.scenario, platform, api);
}
// Cleanup performance entries
this.cleanupPerformanceEntries(ctx);
}
private cleanupPerformanceEntries(ctx: InternalScenarioContext) {
performance.clearMarks(ctx.startMarkName);
ctx.config.requiredPhases.forEach((phase) => {
performance.clearMarks(`scenario_${ctx.scenario}_${phase}`);
});
performance.clearMeasures(`scenario_${ctx.scenario}_total`);
}
private buildSnapshot(
ctx: InternalScenarioContext,
opts: { final: boolean; timedOut: boolean },
): ScenarioContextSnapshot {
const phaseTimings = this.getPhaseTimings(ctx);
// Capture current time once for consistency
const currentTime = performance.now();
// Convert performance timestamps (relative to navigationStart) to absolute timestamps
const navigationStart = performance.timeOrigin;
const startEntry = performance.getEntriesByName(ctx.startMarkName)[0];
const startTimeISO = new Date(navigationStart + (startEntry?.startTime || 0)).toISOString();
const endTimeISO = new Date(navigationStart + currentTime).toISOString();
// Calculate overall scenario duration directly from the timestamps
const durationMs = currentTime - (startEntry?.startTime || 0);
return {
scenario: ctx.scenario,
startTimeISO,
endTimeISO,
durationMs,
completed: Array.from(ctx.completed),
failedPhases: ctx.failed.size > 0 ? Array.from(ctx.failed) : undefined,
timedOut: opts.timedOut,
vitals: { ...this.vitals },
phaseTimings,
};
}
}
export const scenarioMonitor = new ScenarioMonitor();

View File

@@ -0,0 +1,40 @@
import React from "react";
import MetricScenario from "./MetricEvents";
import { useMetricScenario } from "./MetricScenarioProvider";
import { ApplicationMetricPhase, CommonMetricPhase } from "./ScenarioConfig";
/**
* Hook to automatically complete the Interactive phase when the component becomes interactive.
* Uses requestAnimationFrame to complete after the browser has painted.
*/
export function useInteractive(scenario: MetricScenario) {
const { completePhase } = useMetricScenario();
React.useEffect(() => {
requestAnimationFrame(() => {
completePhase(scenario, CommonMetricPhase.Interactive);
});
}, [scenario, completePhase]);
}
/**
* Hook to manage DatabaseLoad scenario phase completions.
* Tracks tree rendering and completes Interactive phase.
* Only completes DatabaseTreeRendered if the database fetch was successful.
* Note: Scenario must be started before databases are fetched (in refreshExplorer).
*/
export function useDatabaseLoadScenario(databaseTreeNodes: unknown[], fetchSucceeded: boolean) {
const { completePhase } = useMetricScenario();
const hasCompletedTreeRenderRef = React.useRef(false);
// Track DatabaseTreeRendered phase (only if fetch succeeded)
React.useEffect(() => {
if (!hasCompletedTreeRenderRef.current && fetchSucceeded) {
hasCompletedTreeRenderRef.current = true;
completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
}
}, [databaseTreeNodes, fetchSucceeded, completePhase]);
// Track Interactive phase
useInteractive(MetricScenario.DatabaseLoad);
}

View File

@@ -114,7 +114,7 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
<div id="connectWithAad"> <div id="connectWithAad">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} /> <input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
{enableConnectionStringLogin && ( {enableConnectionStringLogin && (
<p className="switchConnectTypeText" data-test="Link:SwitchConnectionType" onClick={showForm}> <p className="switchConnectTypeText" data-testid="Link:SwitchConnectionType" onClick={showForm}>
Connect to your account with connection string Connect to your account with connection string
</p> </p>
)} )}

View File

@@ -463,7 +463,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
); );
} }
return ( return (
<div style={{ overflowX: "auto" }} data-test="DataExplorerRoot"> <div style={{ overflowX: "auto" }} data-testid="DataExplorerRoot">
<Stack tokens={containerStackTokens}> <Stack tokens={containerStackTokens}>
<Stack.Item> <Stack.Item>
<CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} /> <CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} />

View File

@@ -2,7 +2,7 @@
exports[`SelfServeComponent message bar and spinner snapshots 1`] = ` exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
<div <div
data-test="DataExplorerRoot" data-testid="DataExplorerRoot"
style={ style={
{ {
"overflowX": "auto", "overflowX": "auto",
@@ -339,7 +339,7 @@ exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
exports[`SelfServeComponent message bar and spinner snapshots 2`] = ` exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
<div <div
data-test="DataExplorerRoot" data-testid="DataExplorerRoot"
style={ style={
{ {
"overflowX": "auto", "overflowX": "auto",
@@ -734,7 +734,7 @@ exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
exports[`SelfServeComponent message bar and spinner snapshots 3`] = ` exports[`SelfServeComponent message bar and spinner snapshots 3`] = `
<div <div
data-test="DataExplorerRoot" data-testid="DataExplorerRoot"
style={ style={
{ {
"overflowX": "auto", "overflowX": "auto",
@@ -835,7 +835,7 @@ exports[`SelfServeComponent message bar and spinner snapshots 4`] = `
exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = ` exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = `
<div <div
data-test="DataExplorerRoot" data-testid="DataExplorerRoot"
style={ style={
{ {
"overflowX": "auto", "overflowX": "auto",

View File

@@ -0,0 +1,37 @@
/**
* Perform a fetch with an AbortController-based timeout. Returns the Response or throws (including AbortError on timeout).
*
* Usage: await fetchWithTimeout(url, { method: 'GET', headers: {...} }, 10000);
*
* A shared helper to remove duplicated inline implementations across the codebase.
*/
export async function fetchWithTimeout(
url: string,
init: RequestInit = {},
timeoutMs: number = 5000,
): Promise<Response> {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { ...init, signal: controller.signal });
return response;
} finally {
clearTimeout(id);
}
}
/**
* Convenience wrapper that returns null instead of throwing on timeout / network error.
* Useful for feature probing scenarios where failure should be treated as absence.
*/
export async function tryFetchWithTimeout(
url: string,
init: RequestInit = {},
timeoutMs: number = 5000,
): Promise<Response | null> {
try {
return await fetchWithTimeout(url, init, timeoutMs);
} catch {
return null;
}
}

View File

@@ -50,3 +50,39 @@ require("jquery-ui-dist/jquery-ui");
// The test environment Data Explorer uses does not have crypto.subtle implementation // The test environment Data Explorer uses does not have crypto.subtle implementation
(<any>global).crypto.subtle = {}; (<any>global).crypto.subtle = {};
// Mock Performance API for scenario monitoring
const performanceMock = {
...(typeof performance !== "undefined" ? performance : {}),
mark: jest.fn(),
measure: jest.fn(),
clearMarks: jest.fn(),
clearMeasures: jest.fn(),
getEntriesByName: jest.fn().mockReturnValue([]),
getEntriesByType: jest.fn().mockReturnValue([]),
now: jest.fn(() => Date.now()),
timeOrigin: Date.now(),
};
// Assign to both global and window
Object.defineProperty(global, "performance", {
writable: true,
configurable: true,
value: performanceMock,
});
Object.defineProperty(window, "performance", {
writable: true,
configurable: true,
value: performanceMock,
});
// Mock fetch API - minimal mock to prevent errors
(<any>global).fetch = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({}),
text: () => Promise.resolve(""),
}),
);

View File

@@ -1,50 +1,114 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import {
deleteContainer,
deleteKeyspace,
openAndFillCreateCassandraTablePanel,
} from "../helpers/containerCreationHelpers";
test("Cassandra keyspace and table CRUD", async ({ page }) => { test("Cassandra: Keyspace and table CRUD", async ({ page }) => {
const keyspaceId = generateUniqueName("db"); const keyspaceId = generateUniqueName("keyspace");
const tableId = "testtable"; // A unique table name isn't needed because the keyspace is unique const tableId = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Cassandra); const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
await explorer.globalCommandButton("New Table").click(); // Create
await explorer.whilePanelOpen( await openAndFillCreateCassandraTablePanel(explorer, {
"Add Table", keyspaceId,
async (panel, okButton) => { tableId,
await panel.getByPlaceholder("Type a new keyspace id").fill(keyspaceId); isAutoscale: true,
await panel.getByPlaceholder("Enter table Id").fill(tableId); throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); });
await okButton.click();
}, const keyspaceNode = await explorer.waitForNode(keyspaceId);
{ closeTimeout: 5 * 60 * 1000 }, const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId);
); await expect(tableNode.element).toBeAttached();
// Delete table
await deleteContainer(explorer, keyspaceId, tableId, "Delete Table");
await expect(tableNode.element).not.toBeAttached();
// Delete keyspace
await deleteKeyspace(explorer, keyspaceId);
await expect(keyspaceNode.element).not.toBeAttached();
});
test("Cassandra: New keyspace shared throughput", async ({ page }) => {
const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId,
useSharedThroughput: true,
});
const keyspaceNode = await explorer.waitForNode(keyspaceId); const keyspaceNode = await explorer.waitForNode(keyspaceId);
const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId); const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId);
await tableNode.openContextMenu(); await expect(tableNode.element).toBeAttached();
await tableNode.contextMenuItem("Delete Table").click();
await explorer.whilePanelOpen(
"Delete Table",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the table id" }).fill(tableId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(tableNode.element).not.toBeAttached();
await keyspaceNode.openContextMenu();
await keyspaceNode.contextMenuItem("Delete Keyspace").click();
await explorer.whilePanelOpen(
"Delete Keyspace",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the Keyspace id" }).fill(keyspaceId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
// Cleanup
await deleteKeyspace(explorer, keyspaceId);
await expect(keyspaceNode.element).not.toBeAttached();
});
test("Cassandra: Manual throughput", async ({ page }) => {
const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId,
isAutoscale: false,
throughputValue: manualThroughput,
});
const keyspaceNode = await explorer.waitForNode(keyspaceId);
const tableNode = await explorer.waitForContainerNode(keyspaceId, tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteKeyspace(explorer, keyspaceId);
await expect(keyspaceNode.element).not.toBeAttached();
});
test("Cassandra: Multiple tables in keyspace", async ({ page }) => {
const keyspaceId = generateUniqueName("keyspace");
const table1Id = generateUniqueName("table");
const table2Id = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
// Create first table
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId: table1Id,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const keyspaceNode = await explorer.waitForNode(keyspaceId);
await explorer.waitForContainerNode(keyspaceId, table1Id);
// Create second table in same keyspace
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId: table2Id,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
await explorer.waitForContainerNode(keyspaceId, table2Id);
// Cleanup
await deleteKeyspace(explorer, keyspaceId);
await expect(keyspaceNode.element).not.toBeAttached(); await expect(keyspaceNode.element).not.toBeAttached();
}); });

View File

@@ -344,7 +344,7 @@ export class DataExplorer {
* There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button.
*/ */
globalCommandButton(label: string): Locator { globalCommandButton(label: string): Locator {
return this.frame.getByTestId("GlobalCommands").getByText(label); return this.frame.getByTestId(`GlobalCommands/Button:${label}`);
} }
/** Select the command bar button with the specified label */ /** Select the command bar button with the specified label */

View File

@@ -1,22 +1,108 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import {
GREMLIN_CONFIG,
deleteContainer,
deleteDatabase,
openAndFillCreateContainerPanel,
} from "../helpers/containerCreationHelpers";
test("Gremlin graph CRUD", async ({ page }) => { test("Gremlin: Database and graph CRUD", async ({ page }) => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const graphId = "testgraph"; // A unique graph name isn't needed because the database is unique const graphId = generateUniqueName("graph");
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
// Create
await openAndFillCreateContainerPanel(explorer, GREMLIN_CONFIG, {
databaseId,
containerId: graphId,
partitionKey: "/pk",
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
// Delete graph
await deleteContainer(explorer, databaseId, graphId, "Delete Graph");
await expect(graphNode.element).not.toBeAttached();
// Delete database
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Gremlin: New database shared throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const graphId = generateUniqueName("graph");
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
await openAndFillCreateContainerPanel(explorer, GREMLIN_CONFIG, {
databaseId,
containerId: graphId,
partitionKey: "/pk",
useSharedThroughput: true,
});
const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Gremlin: Manual throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const graphId = generateUniqueName("graph");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
await openAndFillCreateContainerPanel(explorer, GREMLIN_CONFIG, {
databaseId,
containerId: graphId,
partitionKey: "/pk",
isAutoscale: false,
throughputValue: manualThroughput,
});
const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Gremlin: No unique keys support", async ({ page }) => {
const databaseId = generateUniqueName("db");
const graphId = generateUniqueName("graph");
const explorer = await DataExplorer.open(page, TestAccount.Gremlin); const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
// Create new database and graph
await explorer.globalCommandButton("New Graph").click(); await explorer.globalCommandButton("New Graph").click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Graph", "New Graph",
async (panel, okButton) => { async (panel, okButton) => {
await panel.getByPlaceholder("Type a new database id").fill(databaseId); await panel.getByTestId("AddCollectionPanel/DatabaseId").fill(databaseId);
await panel.getByRole("textbox", { name: "Graph id, Example Graph1" }).fill(graphId); await panel.getByTestId("AddCollectionPanel/CollectionId").fill(graphId);
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); await panel.getByTestId("ThroughputInput/AutoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
// Verify unique key button is not present (Gremlin-specific API limitation)
const uniqueKeyButton = panel.getByTestId("AddCollectionPanel/AddUniqueKeyButton");
await expect(uniqueKeyButton).not.toBeVisible();
await okButton.click(); await okButton.click();
}, },
{ closeTimeout: 5 * 60 * 1000 }, { closeTimeout: 5 * 60 * 1000 },
@@ -24,29 +110,9 @@ test("Gremlin graph CRUD", async ({ page }) => {
const databaseNode = await explorer.waitForNode(databaseId); const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId); const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
await graphNode.openContextMenu(); // Cleanup
await graphNode.contextMenuItem("Delete Graph").click(); await deleteDatabase(explorer, databaseId);
await explorer.whilePanelOpen(
"Delete Graph",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the graph id" }).fill(graphId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(graphNode.element).not.toBeAttached();
await databaseNode.openContextMenu();
await databaseNode.contextMenuItem("Delete Database").click();
await explorer.whilePanelOpen(
"Delete Database",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the Database id" }).fill(databaseId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(databaseNode.element).not.toBeAttached(); await expect(databaseNode.element).not.toBeAttached();
}); });

View File

@@ -0,0 +1,323 @@
import { Locator } from "@playwright/test";
import { DataExplorer, TestAccount } from "../fx";
/**
* Container creation test API configuration
* Defines labels and selectors specific to each Cosmos DB API
*/
export interface ApiConfig {
account: TestAccount;
commandLabel: string; // "New Container", "New Collection", "New Graph", "New Table"
containerIdLabel: string; // "Container id", "Collection id", "Graph id", "Table id"
panelTitle: string; // "New Container", "New Collection", "New Graph", "Add Table"
databaseIdPlaceholder: string; // "Type a new keyspace id" for Cassandra, etc.
containerIdPlaceholder: string;
partitionKeyLabel?: string; // "Partition key", "Shard key", or undefined for Tables
partitionKeyPlaceholder?: string;
confirmDeleteLabel: string; // "Confirm by typing the [container/collection/table/graph] id"
databaseName?: string; // "TablesDB" for Tables, undefined for others
supportsUniqueKeys: boolean;
}
export const SQL_CONFIG: ApiConfig = {
account: TestAccount.SQL,
commandLabel: "New Container",
containerIdLabel: "Container id, Example Container1",
panelTitle: "New Container",
databaseIdPlaceholder: "Type a new database id",
containerIdPlaceholder: "e.g., Container1",
partitionKeyLabel: "Partition key",
partitionKeyPlaceholder: "/pk",
confirmDeleteLabel: "Confirm by typing the container id",
supportsUniqueKeys: true,
};
export const MONGO_CONFIG: ApiConfig = {
account: TestAccount.Mongo,
commandLabel: "New Collection",
containerIdLabel: "Collection id, Example Collection1",
panelTitle: "New Collection",
databaseIdPlaceholder: "Type a new database id",
containerIdPlaceholder: "e.g., Collection1",
partitionKeyLabel: "Shard key",
partitionKeyPlaceholder: "pk",
confirmDeleteLabel: "Confirm by typing the collection id",
supportsUniqueKeys: false,
};
export const MONGO32_CONFIG: ApiConfig = {
...MONGO_CONFIG,
account: TestAccount.Mongo32,
};
export const GREMLIN_CONFIG: ApiConfig = {
account: TestAccount.Gremlin,
commandLabel: "New Graph",
containerIdLabel: "Graph id, Example Graph1",
panelTitle: "New Graph",
databaseIdPlaceholder: "Type a new database id",
containerIdPlaceholder: "e.g., Graph1",
partitionKeyLabel: "Partition key",
partitionKeyPlaceholder: "/pk",
confirmDeleteLabel: "Confirm by typing the graph id",
supportsUniqueKeys: false,
};
export const TABLES_CONFIG: ApiConfig = {
account: TestAccount.Tables,
commandLabel: "New Table",
containerIdLabel: "Table id, Example Table1",
panelTitle: "New Table",
databaseIdPlaceholder: "", // Not used
containerIdPlaceholder: "e.g., Table1",
confirmDeleteLabel: "Confirm by typing the table id",
databaseName: "TablesDB",
supportsUniqueKeys: false,
};
export const CASSANDRA_CONFIG: ApiConfig = {
account: TestAccount.Cassandra,
commandLabel: "New Table",
containerIdLabel: "Enter table Id",
panelTitle: "Add Table",
databaseIdPlaceholder: "Type a new keyspace id",
containerIdPlaceholder: "Enter table Id",
confirmDeleteLabel: "Confirm by typing the table id",
supportsUniqueKeys: false,
};
/**
* Fills database selection in the panel
* Automatically selects "Create new" and fills the database ID
*/
export async function fillDatabaseSelection(panel: Locator, databaseId: string): Promise<void> {
// Wait for the radio button to be visible and click it (more reliable than check for custom styled radios)
await panel.getByTestId("AddCollectionPanel/DatabaseRadio:CreateNew").waitFor({ state: "visible" });
await panel.getByTestId("AddCollectionPanel/DatabaseRadio:CreateNew").click();
await panel.getByTestId("AddCollectionPanel/DatabaseId").fill(databaseId);
}
/**
* Fills existing database selection
* Selects "Use existing" and clicks the dropdown to select the database
*/
export async function fillExistingDatabaseSelection(panel: Locator, databaseId: string): Promise<void> {
await panel.getByTestId("AddCollectionPanel/DatabaseRadio:UseExisting").waitFor({ state: "visible" });
await panel.getByTestId("AddCollectionPanel/DatabaseRadio:UseExisting").click();
await panel.getByTestId("AddCollectionPanel/ExistingDatabaseDropdown").click();
await panel.locator(`text=${databaseId}`).click();
}
/**
* Fills container/collection/graph/table details
*/
export async function fillContainerDetails(
panel: Locator,
containerId: string,
partitionKey: string | undefined,
): Promise<void> {
await panel.getByTestId("AddCollectionPanel/CollectionId").fill(containerId);
if (partitionKey) {
await panel.getByTestId("AddCollectionPanel/PartitionKey").first().fill(partitionKey);
}
}
/**
* Fills Cassandra-specific table details
* (keyspace and table IDs are separate for Cassandra)
*/
export async function fillCassandraTableDetails(panel: Locator, keyspaceId: string, tableId: string): Promise<void> {
await panel.getByTestId("AddCollectionPanel/DatabaseId").fill(keyspaceId);
await panel.getByTestId("AddCollectionPanel/CollectionId").fill(tableId);
}
/**
* Sets throughput mode and value
* @param isAutoscale - if true, sets autoscale mode; if false, sets manual mode
*/
export async function setThroughput(panel: Locator, isAutoscale: boolean, throughputValue: number): Promise<void> {
const testId = isAutoscale ? "ThroughputInput/ThroughputMode:Autoscale" : "ThroughputInput/ThroughputMode:Manual";
await panel.getByTestId(testId).check();
if (isAutoscale) {
await panel.getByTestId("ThroughputInput/AutoscaleRUInput").fill(throughputValue.toString());
} else {
await panel.getByTestId("ThroughputInput/ManualThroughputInput").fill(throughputValue.toString());
}
}
/**
* Adds a unique key to the container (SQL/Mongo only)
*/
export async function addUniqueKey(panel: Locator, uniqueKeyValue: string): Promise<void> {
// Scroll to find the unique key section
await panel.getByTestId("AddCollectionPanel/UniqueKeysSection").scrollIntoViewIfNeeded();
// Click the "Add unique key" button
await panel.getByTestId("AddCollectionPanel/AddUniqueKeyButton").click();
// Fill in the unique key value
const uniqueKeyInput = panel.getByTestId("AddCollectionPanel/UniqueKey").first();
await uniqueKeyInput.fill(uniqueKeyValue);
}
/**
* Deletes a database and waits for it to disappear from the tree
*/
export async function deleteDatabase(
explorer: DataExplorer,
databaseId: string,
databaseNodeName: string = databaseId,
): Promise<void> {
const databaseNode = await explorer.waitForNode(databaseNodeName);
await databaseNode.openContextMenu();
await databaseNode.contextMenuItem("Delete Database").click();
await explorer.whilePanelOpen(
"Delete Database",
async (panel: Locator, okButton: Locator) => {
await panel.getByTestId("DeleteDatabaseConfirmationPanel/ConfirmInput").fill(databaseId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}
/**
* Deletes a keyspace (Cassandra only)
*/
export async function deleteKeyspace(explorer: DataExplorer, keyspaceId: string): Promise<void> {
const keyspaceNode = await explorer.waitForNode(keyspaceId);
await keyspaceNode.openContextMenu();
await keyspaceNode.contextMenuItem("Delete Keyspace").click();
await explorer.whilePanelOpen(
"Delete Keyspace",
async (panel: Locator, okButton: Locator) => {
await panel.getByTestId("DeleteCollectionConfirmationPane/ConfirmInput").fill(keyspaceId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}
/**
* Deletes a container/collection/graph/table
*/
export async function deleteContainer(
explorer: DataExplorer,
databaseId: string,
containerId: string,
deleteLabel: string, // "Delete Container", "Delete Collection", etc.
): Promise<void> {
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await containerNode.openContextMenu();
await containerNode.contextMenuItem(deleteLabel).click();
await explorer.whilePanelOpen(
deleteLabel,
async (panel: Locator, okButton: Locator) => {
// All container/collection/graph/table deletes use same panel with test ID
await panel.getByTestId("DeleteCollectionConfirmationPane/ConfirmInput").fill(containerId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}
/**
* Opens the create container dialog and fills in the form based on scenario
*/
export async function openAndFillCreateContainerPanel(
explorer: DataExplorer,
config: ApiConfig,
options: {
databaseId: string;
containerId: string;
partitionKey?: string;
useExistingDatabase?: boolean;
isAutoscale?: boolean;
throughputValue?: number;
uniqueKey?: string;
useSharedThroughput?: boolean;
},
): Promise<void> {
await explorer.globalCommandButton(config.commandLabel).click();
await explorer.whilePanelOpen(
config.panelTitle,
async (panel, okButton) => {
// Database selection
if (options.useExistingDatabase) {
await fillExistingDatabaseSelection(panel, options.databaseId);
} else {
await fillDatabaseSelection(panel, options.databaseId);
}
// Shared throughput checkbox (if applicable)
if (options.useSharedThroughput) {
await panel
.getByTestId("AddCollectionPanel/SharedThroughputCheckbox")
.getByRole("checkbox")
.check({ force: true });
}
// Container details
await fillContainerDetails(panel, options.containerId, options.partitionKey);
// Throughput (only if not using shared throughput)
if (!options.useSharedThroughput) {
const isAutoscale = options.isAutoscale !== false;
const throughputValue = options.throughputValue || 1000;
await setThroughput(panel, isAutoscale, throughputValue);
}
// Unique keys (if applicable)
if (options.uniqueKey && config.supportsUniqueKeys) {
await addUniqueKey(panel, options.uniqueKey);
}
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}
/**
* Opens the create table dialog for Cassandra and fills in the form
* Cassandra has a different UI pattern than other APIs
*/
export async function openAndFillCreateCassandraTablePanel(
explorer: DataExplorer,
options: {
keyspaceId: string;
tableId: string;
isAutoscale?: boolean;
throughputValue?: number;
useSharedThroughput?: boolean;
},
): Promise<void> {
await explorer.globalCommandButton("New Table").click();
await explorer.whilePanelOpen(
"Add Table",
async (panel, okButton) => {
// Fill Cassandra-specific table details
await fillCassandraTableDetails(panel, options.keyspaceId, options.tableId);
// Shared throughput checkbox (if applicable)
if (options.useSharedThroughput) {
await panel
.getByTestId("AddCollectionPanel/SharedThroughputCheckbox")
.getByRole("checkbox")
.check({ force: true });
}
// Throughput (only if not using shared throughput)
if (!options.useSharedThroughput) {
const isAutoscale = options.isAutoscale !== false;
const throughputValue = options.throughputValue || 1000;
await setThroughput(panel, isAutoscale, throughputValue);
}
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
}

View File

@@ -1,58 +1,118 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import {
MONGO32_CONFIG,
MONGO_CONFIG,
deleteContainer,
deleteDatabase,
openAndFillCreateContainerPanel,
} from "../helpers/containerCreationHelpers";
( (
[ [
["latest API version", TestAccount.Mongo], ["latest API version", MONGO_CONFIG],
["3.2 API", TestAccount.Mongo32], ["3.2 API", MONGO32_CONFIG],
] as [string, TestAccount][] ] as [string, typeof MONGO_CONFIG][]
).forEach(([apiVersionDescription, accountType]) => { ).forEach(([apiVersionDescription, config]) => {
test(`Mongo CRUD using ${apiVersionDescription}`, async ({ page }) => { test(`Mongo: Database and collection CRUD using ${apiVersionDescription}`, async ({ page }) => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const collectionId = "testcollection"; // A unique collection name isn't needed because the database is unique const collectionId = generateUniqueName("collection");
const explorer = await DataExplorer.open(page, accountType); const explorer = await DataExplorer.open(page, config.account);
await explorer.globalCommandButton("New Collection").click(); // Create
await explorer.whilePanelOpen( await openAndFillCreateContainerPanel(explorer, config, {
"New Collection", databaseId,
async (panel, okButton) => { containerId: collectionId,
await panel.getByPlaceholder("Type a new database id").fill(databaseId); partitionKey: "pk",
await panel.getByRole("textbox", { name: "Collection id, Example Collection1" }).fill(collectionId); isAutoscale: true,
await panel.getByRole("textbox", { name: "Shard key" }).fill("pk"); throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); });
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
const databaseNode = await explorer.waitForNode(databaseId); const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId); const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
await collectionNode.openContextMenu(); // Delete collection
await collectionNode.contextMenuItem("Delete Collection").click(); await deleteContainer(explorer, databaseId, collectionId, "Delete Collection");
await explorer.whilePanelOpen(
"Delete Collection",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the collection id" }).fill(collectionId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(collectionNode.element).not.toBeAttached(); await expect(collectionNode.element).not.toBeAttached();
await databaseNode.openContextMenu(); // Delete database
await databaseNode.contextMenuItem("Delete Database").click(); await deleteDatabase(explorer, databaseId);
await explorer.whilePanelOpen(
"Delete Database",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the Database id" }).fill(databaseId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(databaseNode.element).not.toBeAttached(); await expect(databaseNode.element).not.toBeAttached();
}); });
}); });
test("Mongo: New database shared throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const collectionId = generateUniqueName("collection");
const explorer = await DataExplorer.open(page, TestAccount.Mongo);
await openAndFillCreateContainerPanel(explorer, MONGO_CONFIG, {
databaseId,
containerId: collectionId,
partitionKey: "pk",
useSharedThroughput: true,
});
const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Mongo: Unique keys", async ({ page }) => {
const databaseId = generateUniqueName("db");
const collectionId = generateUniqueName("collection");
const explorer = await DataExplorer.open(page, TestAccount.Mongo);
await openAndFillCreateContainerPanel(explorer, MONGO_CONFIG, {
databaseId,
containerId: collectionId,
partitionKey: "pk",
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
uniqueKey: "email",
});
const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("Mongo: Manual throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const collectionId = generateUniqueName("collection");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.Mongo);
await openAndFillCreateContainerPanel(explorer, MONGO_CONFIG, {
databaseId,
containerId: collectionId,
partitionKey: "pk",
isAutoscale: false,
throughputValue: manualThroughput,
});
const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});

View File

@@ -1,51 +1,110 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import {
SQL_CONFIG,
deleteContainer,
deleteDatabase,
openAndFillCreateContainerPanel,
} from "../helpers/containerCreationHelpers";
test("SQL database and container CRUD", async ({ page }) => { test("SQL: Database and container CRUD", async ({ page }) => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique const containerId = generateUniqueName("container");
const explorer = await DataExplorer.open(page, TestAccount.SQL); const explorer = await DataExplorer.open(page, TestAccount.SQL);
await explorer.globalCommandButton("New Container").click(); // Create
await explorer.whilePanelOpen( await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, {
"New Container", databaseId,
async (panel, okButton) => { containerId,
await panel.getByPlaceholder("Type a new database id").fill(databaseId); partitionKey: "/pk",
await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); isAutoscale: true,
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); });
await okButton.click();
}, const databaseNode = await explorer.waitForNode(databaseId);
{ closeTimeout: 5 * 60 * 1000 }, const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
); await expect(containerNode.element).toBeAttached();
// Delete container
await deleteContainer(explorer, databaseId, containerId, "Delete Container");
await expect(containerNode.element).not.toBeAttached();
// Delete database
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("SQL: New database shared throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container");
const explorer = await DataExplorer.open(page, TestAccount.SQL);
await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, {
databaseId,
containerId,
partitionKey: "/pk",
useSharedThroughput: true,
});
const databaseNode = await explorer.waitForNode(databaseId); const databaseNode = await explorer.waitForNode(databaseId);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId); const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await containerNode.openContextMenu(); await expect(containerNode.element).toBeAttached();
await containerNode.contextMenuItem("Delete Container").click();
await explorer.whilePanelOpen(
"Delete Container",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the container id" }).fill(containerId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(containerNode.element).not.toBeAttached();
await databaseNode.openContextMenu();
await databaseNode.contextMenuItem("Delete Database").click();
await explorer.whilePanelOpen(
"Delete Database",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the database id" }).fill(databaseId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("SQL: Unique keys", async ({ page }) => {
const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container");
const explorer = await DataExplorer.open(page, TestAccount.SQL);
await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, {
databaseId,
containerId,
partitionKey: "/pk",
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
uniqueKey: "/email,/username",
});
const databaseNode = await explorer.waitForNode(databaseId);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await expect(containerNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached();
});
test("SQL: Manual throughput", async ({ page }) => {
const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.SQL);
await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, {
databaseId,
containerId,
partitionKey: "/pk",
isAutoscale: false,
throughputValue: manualThroughput,
});
const databaseNode = await explorer.waitForNode(databaseId);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await expect(containerNode.element).toBeAttached();
// Cleanup
await deleteDatabase(explorer, databaseId);
await expect(databaseNode.element).not.toBeAttached(); await expect(databaseNode.element).not.toBeAttached();
}); });

View File

@@ -1,35 +1,116 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx"; import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import { TABLES_CONFIG, deleteContainer, openAndFillCreateContainerPanel } from "../helpers/containerCreationHelpers";
test("Tables CRUD", async ({ page }) => { test("Tables: CRUD", async ({ page }) => {
const tableId = generateUniqueName("table"); // A unique table name IS needed because the database is shared when using Table Storage. const tableId = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Tables); const explorer = await DataExplorer.open(page, TestAccount.Tables);
await explorer.globalCommandButton("New Table").click(); // Create
await explorer.whilePanelOpen( await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
"New Table", databaseId: "TablesDB",
async (panel, okButton) => { containerId: tableId,
await panel.getByRole("textbox", { name: "Table id, Example Table1" }).fill(tableId); isAutoscale: true,
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
await okButton.click(); });
},
{ closeTimeout: 5 * 60 * 1000 },
);
const tableNode = await explorer.waitForContainerNode("TablesDB", tableId); const tableNode = await explorer.waitForContainerNode("TablesDB", tableId);
await expect(tableNode.element).toBeAttached();
await tableNode.openContextMenu(); // Delete table
await tableNode.contextMenuItem("Delete Table").click(); await deleteContainer(explorer, "TablesDB", tableId, "Delete Table");
await explorer.whilePanelOpen( await expect(tableNode.element).not.toBeAttached();
"Delete Table", });
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the table id" }).fill(tableId); test("Tables: New database shared throughput", async ({ page }) => {
await okButton.click(); const tableId = generateUniqueName("table");
},
{ closeTimeout: 5 * 60 * 1000 }, const explorer = await DataExplorer.open(page, TestAccount.Tables);
);
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: tableId,
useSharedThroughput: true,
});
const tableNode = await explorer.waitForContainerNode("TablesDB", tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteContainer(explorer, "TablesDB", tableId, "Delete Table");
await expect(tableNode.element).not.toBeAttached();
});
test("Tables: Manual throughput", async ({ page }) => {
const tableId = generateUniqueName("table");
const manualThroughput = 400;
const explorer = await DataExplorer.open(page, TestAccount.Tables);
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: tableId,
isAutoscale: false,
throughputValue: manualThroughput,
});
const tableNode = await explorer.waitForContainerNode("TablesDB", tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteContainer(explorer, "TablesDB", tableId, "Delete Table");
await expect(tableNode.element).not.toBeAttached();
});
test("Tables: Multiple tables in TablesDB", async ({ page }) => {
const table1Id = generateUniqueName("table");
const table2Id = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Tables);
// Create first table
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: table1Id,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
await explorer.waitForContainerNode("TablesDB", table1Id);
// Create second table
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: table2Id,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
await explorer.waitForContainerNode("TablesDB", table2Id);
// Cleanup
await deleteContainer(explorer, "TablesDB", table1Id, "Delete Table");
await deleteContainer(explorer, "TablesDB", table2Id, "Delete Table");
});
test("Tables: No partition key support", async ({ page }) => {
const tableId = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Tables);
await openAndFillCreateContainerPanel(explorer, TABLES_CONFIG, {
databaseId: "TablesDB",
containerId: tableId,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const tableNode = await explorer.waitForContainerNode("TablesDB", tableId);
await expect(tableNode.element).toBeAttached();
// Cleanup
await deleteContainer(explorer, "TablesDB", tableId, "Delete Table");
await expect(tableNode.element).not.toBeAttached(); await expect(tableNode.element).not.toBeAttached();
}); });

View File

@@ -134,7 +134,7 @@ const initTestExplorer = async (): Promise<void> => {
); );
iframe.id = "explorerMenu"; iframe.id = "explorerMenu";
iframe.name = "explorer"; iframe.name = "explorer";
iframe.setAttribute("data-test", "DataExplorerFrame"); iframe.setAttribute("data-testid", "DataExplorerFrame");
iframe.classList.add("iframe"); iframe.classList.add("iframe");
iframe.title = "explorer"; iframe.title = "explorer";
iframe.src = iframeSrc; // CodeQL [SM03712] Not used in production, only for testing purposes iframe.src = iframeSrc; // CodeQL [SM03712] Not used in production, only for testing purposes