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
82 changed files with 1857 additions and 1120 deletions

View File

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

6
package-lock.json generated
View File

@@ -116,6 +116,7 @@
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0",
"web-vitals": "4.2.4",
"uuid": "9.0.0",
"zustand": "3.5.0"
},
@@ -35930,6 +35931,11 @@
"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": {
"version": "3.0.1",
"license": "BSD-2-Clause"

View File

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

View File

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

View File

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

View File

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

View File

@@ -137,7 +137,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
)}
<div
data-test="EditorReact/Host/Unloaded"
data-testid="EditorReact/Host/Unloaded"
className={this.props.className || "jsonEditor"}
style={this.props.monacoContainerStyles}
ref={(elt: HTMLElement) => this.setRef(elt)}
@@ -148,7 +148,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
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
if (process.env.NODE_ENV === "development") {

View File

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

View File

@@ -1482,9 +1482,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
itemKey: SettingsV2TabTypes[tab.tab],
style: { marginTop: 20 },
headerText: getTabTitle(tab.tab),
headerButtonProps: {
"data-test": `settings-tab-header/${SettingsV2TabTypes[tab.tab]}`,
},
};
return (

View File

@@ -127,9 +127,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
};
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", ariaLabel: "ttl-off-option" },
{ key: TtlType.OnNoDefault, text: "On (no default)", ariaLabel: "ttl-on-no-default-option" },
{ key: TtlType.On, text: "On", ariaLabel: "ttl-on-option" },
{ key: TtlType.Off, text: "Off" },
{ key: TtlType.OnNoDefault, text: "On (no default)" },
{ key: TtlType.On, text: "On" },
];
public getTtlValue = (value: string): TtlType => {
@@ -223,7 +223,6 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
ariaLabel={`Time to live in seconds`}
data-test="ttl-input"
/>
)}
</Stack>

View File

@@ -503,9 +503,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<span style={{ float: "left", transform: "translateX(-50%)" }}>
{this.props.instantMaximumThroughput.toLocaleString()}
</span>
<span style={{ float: "right" }} data-test="soft-allowed-maximum-throughput">
{this.props.softAllowedMaximumThroughput.toLocaleString()}
</span>
<span style={{ float: "right" }}>{this.props.softAllowedMaximumThroughput.toLocaleString()}</span>
</Stack.Item>
</Stack>
<ProgressIndicator
@@ -628,12 +626,11 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
min={autoPilotThroughput1K}
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
const errorMessage: string =
sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError;
return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
return sanitizedValue % 1000
? "Throughput value must be in increments of 1000"
: this.props.throughputError;
}}
validateOnLoad={false}
data-test="autopilot-throughput-input"
/>
</Stack>
</Stack>
@@ -653,10 +650,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
}
onChange={this.onThroughputChange}
min={this.props.minimum}
onGetErrorMessage={() => {
return <span data-test="manual-throughput-input-error">{this.props.throughputError}</span>;
}}
data-test="manual-throughput-input"
errorMessage={this.props.throughputError}
/>
)}
</>

View File

@@ -273,7 +273,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
/>
</Stack>
<StyledTextFieldBase
data-test="autopilot-throughput-input"
disabled={true}
id="autopilotInput"
key="auto pilot throughput input"
@@ -334,7 +333,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
5,000
</span>
<span
data-test="soft-allowed-maximum-throughput"
style={
{
"float": "right",
@@ -754,13 +752,11 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
}
>
<StyledTextFieldBase
data-test="manual-throughput-input"
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
onGetErrorMessage={[Function]}
required={true}
step={100}
styles={
@@ -815,7 +811,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
5,000
</span>
<span
data-test="soft-allowed-maximum-throughput"
style={
{
"float": "right",
@@ -1211,13 +1206,11 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
}
>
<StyledTextFieldBase
data-test="manual-throughput-input"
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
onGetErrorMessage={[Function]}
required={true}
step={100}
styles={
@@ -1272,7 +1265,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
5,000
</span>
<span
data-test="soft-allowed-maximum-throughput"
style={
{
"float": "right",

View File

@@ -22,17 +22,14 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
options={
[
{
"ariaLabel": "ttl-off-option",
"key": "off",
"text": "Off",
},
{
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault",
"text": "On (no default)",
},
{
"ariaLabel": "ttl-on-option",
"key": "on",
"text": "On",
},
@@ -66,7 +63,6 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
/>
<StyledTextFieldBase
ariaLabel="Time to live in seconds"
data-test="ttl-input"
id="timeToLiveSeconds"
max={2147483647}
min={1}
@@ -288,17 +284,14 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
options={
[
{
"ariaLabel": "ttl-off-option",
"key": "off",
"text": "Off",
},
{
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault",
"text": "On (no default)",
},
{
"ariaLabel": "ttl-on-option",
"key": "on",
"text": "On",
},
@@ -332,7 +325,6 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
/>
<StyledTextFieldBase
ariaLabel="Time to live in seconds"
data-test="ttl-input"
id="timeToLiveSeconds"
max={2147483647}
min={1}
@@ -609,17 +601,14 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
options={
[
{
"ariaLabel": "ttl-off-option",
"key": "off",
"text": "Off",
},
{
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault",
"text": "On (no default)",
},
{
"ariaLabel": "ttl-on-option",
"key": "on",
"text": "On",
},
@@ -653,7 +642,6 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
/>
<StyledTextFieldBase
ariaLabel="Time to live in seconds"
data-test="ttl-input"
id="timeToLiveSeconds"
max={2147483647}
min={1}
@@ -890,17 +878,14 @@ exports[`SubSettingsComponent renders 1`] = `
options={
[
{
"ariaLabel": "ttl-off-option",
"key": "off",
"text": "Off",
},
{
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault",
"text": "On (no default)",
},
{
"ariaLabel": "ttl-on-option",
"key": "on",
"text": "On",
},
@@ -934,7 +919,6 @@ exports[`SubSettingsComponent renders 1`] = `
/>
<StyledTextFieldBase
ariaLabel="Time to live in seconds"
data-test="ttl-input"
id="timeToLiveSeconds"
max={2147483647}
min={1}
@@ -1236,17 +1220,14 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
options={
[
{
"ariaLabel": "ttl-off-option",
"key": "off",
"text": "Off",
},
{
"ariaLabel": "ttl-on-no-default-option",
"key": "on-nodefault",
"text": "On (no default)",
},
{
"ariaLabel": "ttl-on-option",
"key": "on",
"text": "On",
},

View File

@@ -12,11 +12,6 @@ exports[`SettingsComponent renders 1`] = `
selectedKey="ScaleTab"
>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/ScaleTab",
}
}
headerText="Scale"
itemKey="ScaleTab"
key="ScaleTab"
@@ -107,11 +102,6 @@ exports[`SettingsComponent renders 1`] = `
/>
</PivotItem>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/SubSettingsTab",
}
}
headerText="Settings"
itemKey="SubSettingsTab"
key="SubSettingsTab"
@@ -211,11 +201,6 @@ exports[`SettingsComponent renders 1`] = `
/>
</PivotItem>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/ContainerVectorPolicyTab",
}
}
headerText="Container Policies"
itemKey="ContainerVectorPolicyTab"
key="ContainerVectorPolicyTab"
@@ -242,11 +227,6 @@ exports[`SettingsComponent renders 1`] = `
/>
</PivotItem>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/IndexingPolicyTab",
}
}
headerText="Indexing Policy"
itemKey="IndexingPolicyTab"
key="IndexingPolicyTab"
@@ -283,11 +263,6 @@ exports[`SettingsComponent renders 1`] = `
/>
</PivotItem>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/PartitionKeyTab",
}
}
headerText="Partition Keys (preview)"
itemKey="PartitionKeyTab"
key="PartitionKeyTab"
@@ -395,11 +370,6 @@ exports[`SettingsComponent renders 1`] = `
/>
</PivotItem>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/ComputedPropertiesTab",
}
}
headerText="Computed Properties"
itemKey="ComputedPropertiesTab"
key="ComputedPropertiesTab"
@@ -434,11 +404,6 @@ exports[`SettingsComponent renders 1`] = `
/>
</PivotItem>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/GlobalSecondaryIndexTab",
}
}
headerText="Global Secondary Index (Preview)"
itemKey="GlobalSecondaryIndexTab"
key="GlobalSecondaryIndexTab"

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,9 @@ import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse
import * as ViewModels from "../Contracts/ViewModels";
import { UploadDetailsRecord } from "../Contracts/ViewModels";
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 * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@@ -402,7 +405,9 @@ export default class Explorer {
updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) =>
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);
} catch (error) {
const errorMessage = getErrorMessage(error);
@@ -416,6 +421,8 @@ export default class Explorer {
startKey,
);
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> {
// Start DatabaseLoad scenario before fetching databases
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
scenarioMonitor.start(MetricScenario.DatabaseLoad);
}
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()

View File

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

View File

@@ -127,13 +127,13 @@ export class NotificationConsoleComponent extends React.Component<
</span>
</span>
<span className="consoleSplitter" />
<span className="headerStatus" data-test="notification-console/header-status">
<span className="headerStatus">
<span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true">
{this.state.headerStatus}
</span>
</span>
</div>
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
<div className="expandCollapseButton" data-testid="NotificationConsole/ExpandCollapseButton">
<img
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
@@ -145,7 +145,7 @@ export class NotificationConsoleComponent extends React.Component<
height={this.props.isConsoleExpanded ? "auto" : 0}
onAnimationEnd={this.onConsoleWasExpanded}
>
<div data-test="NotificationConsole/Contents" className="notificationConsoleContents">
<div data-testid="NotificationConsole/Contents" className="notificationConsoleContents">
<div className="notificationConsoleControls">
<Dropdown
label="Filter:"

View File

@@ -78,7 +78,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
/>
<span
className="headerStatus"
data-test="notification-console/header-status"
>
<span
aria-atomic="true"
@@ -89,7 +88,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</div>
<div
className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
data-testid="NotificationConsole/ExpandCollapseButton"
>
<img
alt="Expand icon"
@@ -123,7 +122,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
>
<div
className="notificationConsoleContents"
data-test="NotificationConsole/Contents"
data-testid="NotificationConsole/Contents"
>
<div
className="notificationConsoleControls"
@@ -262,7 +261,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
/>
<span
className="headerStatus"
data-test="notification-console/header-status"
>
<span
aria-atomic="true"
@@ -275,7 +273,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</div>
<div
className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
data-testid="NotificationConsole/ExpandCollapseButton"
>
<img
alt="Expand icon"
@@ -309,7 +307,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
>
<div
className="notificationConsoleContents"
data-test="NotificationConsole/Contents"
data-testid="NotificationConsole/Contents"
>
<div
className="notificationConsoleControls"

View File

@@ -56,16 +56,16 @@ export class StatusBar extends React.Component<Props> {
return (
<BarContainer>
<Bar data-test="notebookStatusBar">
<Bar data-testid="notebookStatusBar">
<RightStatus>
{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>
)}
</RightStatus>
<LeftStatus>
<p data-test="kernelStatus">
<p data-testid="kernelStatus">
{name} | {this.props.kernelStatus}
</p>
</LeftStatus>

View File

@@ -7,7 +7,6 @@ import {
Icon,
IconButton,
IDropdownOption,
IRenderFunction,
Link,
ProgressIndicator,
Separator,
@@ -302,6 +301,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
type="radio"
role="radio"
id="databaseCreateNew"
data-testid="AddCollectionPanel/DatabaseRadio:CreateNew"
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
@@ -315,6 +315,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
name="databaseType"
type="radio"
role="radio"
data-testid="AddCollectionPanel/DatabaseRadio:UseExisting"
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
@@ -338,6 +339,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
size={40}
className="panelTextField"
aria-label="New database id, Type a new database id"
data-testid="AddCollectionPanel/DatabaseId"
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -347,18 +349,20 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ isSharedThroughputChecked: isChecked })
}
/>
<div data-testid="AddCollectionPanel/SharedThroughputCheckbox">
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
this.setState({ isSharedThroughputChecked: isChecked })
}
/>
</div>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
@@ -397,6 +401,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!this.state.createNewDatabase && (
<Dropdown
ariaLabel="Choose an existing database"
data-testid="AddCollectionPanel/ExistingDatabaseDropdown"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database"
@@ -406,7 +411,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
defaultSelectedKey={this.props.databaseId}
responsiveMode={999}
onRenderOption={this.onRenderDatabaseOption}
/>
)}
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
@@ -445,6 +449,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"
data-testid="AddCollectionPanel/CollectionId"
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
value={this.state.collectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -578,6 +583,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
type="text"
id="addCollection-partitionKeyValue"
data-testid="AddCollectionPanel/PartitionKey"
aria-required
required
size={40}
@@ -614,6 +620,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
type="text"
id="addCollection-partitionKeyValue"
data-testid="AddCollectionPanel/PartitionKey"
key={`addCollection-partitionKeyValue_${index}`}
aria-required
required
@@ -692,8 +699,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
directionalHint={DirectionalHint.bottomLeftEdge}
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true,
).toLocaleLowerCase()} in the database and
true,
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
>
@@ -703,8 +710,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true,
).toLocaleLowerCase()} in the database and
true,
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
/>
@@ -731,7 +738,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!isFabricNative() && userContext.apiType === "SQL" && (
<Stack style={{ marginTop: -2, marginBottom: -4 }}>
<Stack style={{ marginTop: -2, marginBottom: -4 }} data-testid="AddCollectionPanel/UniqueKeysSection">
{UniqueKeysHeader()}
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
return (
@@ -745,6 +752,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
data-testid="AddCollectionPanel/UniqueKey"
value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
@@ -771,6 +779,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<ActionButton
iconProps={{ iconName: "Add" }}
data-testid="AddCollectionPanel/AddUniqueKeyButton"
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })}
>
@@ -883,8 +892,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
}}
//TODO: uncomment when learn more text becomes available
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
//TODO: uncomment when learn more text becomes available
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
>
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
@@ -1302,15 +1311,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
const partitionKey: DataModels.PartitionKey = partitionKeyString
? {
paths: [
partitionKeyString,
...(userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0
? this.state.subPartitionKeys
: []),
],
kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
version: partitionKeyVersion,
}
paths: [
partitionKeyString,
...(userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0
? this.state.subPartitionKeys
: []),
],
kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
version: partitionKeyVersion,
}
: undefined;
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
@@ -1435,19 +1444,4 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
TelemetryProcessor.traceFailure(Action.CreateCollection, failureTelemetryData, startKey);
}
}
private onRenderDatabaseOption = (
option?: IDropdownOption,
defaultRender?: (props?: IDropdownOption) => JSX.Element,
): JSX.Element | null => {
if (!option) {
return null;
}
return (
<div data-testid={`database-option-${option.key}`}>
{defaultRender ? defaultRender(option) : <span>{option.text}</span>}
</div>
);
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,15 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
jest.mock("rx-jupyter", () => ({
sessions: {
create: jest.fn(),
},
contents: {
JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
},
}));
jest.mock("Common/dataAccess/queryDocuments", () => ({
queryDocuments: jest.fn(() => ({
// 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 (
<CosmosFluentProvider className={styles.container}>
<div data-test={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
<div data-test={"DocumentsTab/Filter"} className={`${styles.filterRow} ${styles.smallScreenContent}`}>
<div data-testid={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
<div data-testid={"DocumentsTab/Filter"} className={`${styles.filterRow} ${styles.smallScreenContent}`}>
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
<InputDataList
dropdownOptions={getFilterChoices()}
@@ -2164,7 +2164,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
/>
<Button
appearance="primary"
data-test={"DocumentsTab/ApplyFilter"}
data-testid={"DocumentsTab/ApplyFilter"}
size="small"
onClick={() => {
if (isExecuting) {
@@ -2191,7 +2191,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
>
<Allotment.Pane preferredSize={`${tabStateData.leftPaneWidthPercent}%`} minSize={55}>
<div
data-test={"DocumentsTab/DocumentsPane"}
data-testid={"DocumentsTab/DocumentsPane"}
style={{ height: "100%", width: "100%", overflow: "hidden" }}
ref={tableContainerRef}
>
@@ -2237,7 +2237,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
{tableItems.length > 0 && (
<a
className={styles.loadMore}
data-test={"DocumentsTab/LoadMore"}
data-testid={"DocumentsTab/LoadMore"}
role="button"
tabIndex={0}
onClick={() => loadNextPage(documentsIterator.iterator, false)}
@@ -2249,7 +2249,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
</div>
</Allotment.Pane>
<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 && (
<EditorReact
language={"json"}

View File

@@ -19,6 +19,15 @@ import { act } from "react-dom/test-utils";
import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
jest.mock("rx-jupyter", () => ({
sessions: {
create: jest.fn(),
},
contents: {
JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
},
}));
jest.requireActual("Explorer/Controls/Editor/EditorReact");
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
className="tab-pane active"
data-test="DocumentsTab"
data-testid="DocumentsTab"
role="tabpanel"
style={
{
@@ -16,7 +16,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
>
<div
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>
SELECT * FROM c
@@ -51,7 +51,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
<Button
appearance="primary"
aria-label="Apply filter"
data-test="DocumentsTab/ApplyFilter"
data-testid="DocumentsTab/ApplyFilter"
disabled={false}
onClick={[Function]}
size="small"
@@ -68,7 +68,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
preferredSize="35%"
>
<div
data-test="DocumentsTab/DocumentsPane"
data-testid="DocumentsTab/DocumentsPane"
style={
{
"height": "100%",
@@ -130,7 +130,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
minSize={30}
>
<div
data-test="DocumentsTab/ResultsPane"
data-testid="DocumentsTab/ResultsPane"
style={
{
"height": "100%",

View File

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

View File

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

View File

@@ -64,7 +64,7 @@ describe("QueryTabComponent", () => {
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 });
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);

View File

@@ -746,7 +746,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}}
>
<Allotment.Pane
data-test="QueryTab/EditorPane"
data-testid="QueryTab/EditorPane"
preferredSize={
this.state.queryViewSizePercent !== undefined ? `${this.state.queryViewSizePercent}%` : undefined
}
@@ -813,7 +813,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
render(): JSX.Element {
const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive;
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%" }}>
{this.getEditorAndQueryResult()}
</div>

View File

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

View File

@@ -237,14 +237,14 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
if (tab) {
if ("render" in tab) {
return (
<div data-test={`Tab:${tab.tabId}`} {...attrs}>
<div data-testid={`Tab:${tab.tabId}`} {...attrs}>
{tab.render()}
</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 => {

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ interface DatabasesState {
databases: ViewModels.Database[];
resourceTokenCollection: ViewModels.CollectionBase;
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
databasesFetchedSuccessfully: boolean; // Track if last database fetch was successful
updateDatabase: (database: ViewModels.Database) => void;
addDatabases: (databases: ViewModels.Database[]) => void;
deleteDatabase: (database: ViewModels.Database) => void;
@@ -30,6 +31,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
databases: [],
resourceTokenCollection: undefined,
sampleDataResourceTokenCollection: undefined,
databasesFetchedSuccessfully: false,
updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => {
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
key={databaseAccount?.id || encryptedTokenMetadata?.accountName || authType}
ref={ref}
data-test="DataExplorerFrame"
data-testid="DataExplorerFrame"
id="explorerMenu"
name="explorer"
className="iframe"

View File

@@ -60,6 +60,10 @@ import "./Explorer/Panes/PanelComponent.less";
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
import "./Explorer/SplashScreen/SplashScreen.less";
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 "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig";
@@ -79,13 +83,27 @@ const App: React.FunctionComponent = () => {
StyleConstants.updateStyles();
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) {
return <LoadingExplorer />;
}
return (
<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" ? (
<ContainerCopyPanel explorer={explorer} />
) : (
@@ -104,9 +122,16 @@ const App: React.FunctionComponent = () => {
};
const mainElement = document.getElementById("Main");
ReactDOM.render(<App />, mainElement);
ReactDOM.render(
<MetricScenarioProvider>
<App />
</MetricScenarioProvider>,
mainElement,
);
function DivExplorer({ explorer }: { explorer: Explorer }): JSX.Element {
useInteractive(MetricScenario.ApplicationLoad);
return (
<div id="divExplorer" className="flexContainer hideOverflows">
<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">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
{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
</p>
)}

View File

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

View File

@@ -2,7 +2,7 @@
exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
<div
data-test="DataExplorerRoot"
data-testid="DataExplorerRoot"
style={
{
"overflowX": "auto",
@@ -339,7 +339,7 @@ exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
<div
data-test="DataExplorerRoot"
data-testid="DataExplorerRoot"
style={
{
"overflowX": "auto",
@@ -734,7 +734,7 @@ exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
exports[`SelfServeComponent message bar and spinner snapshots 3`] = `
<div
data-test="DataExplorerRoot"
data-testid="DataExplorerRoot"
style={
{
"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`] = `
<div
data-test="DataExplorerRoot"
data-testid="DataExplorerRoot"
style={
{
"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
(<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

@@ -40,13 +40,13 @@ To use this script, there are a few prerequisites that must be done at least onc
5. Ensure you have a Resource Group _ready_ to deploy into, the deploy script requires an existing resource group. This resource group should be named `[username]-e2e-testing`, where `[username]` is your Windows username, (**Microsoft employees:** This should be your alias). The easiest way to do this is by running the `create-resource-group.ps1` script, specifying the Subscription (Name or ID) and Location in which you want to create the Resource Group. For example:
```powershell
.\test\resources\create-resource-group.ps1 -SubscriptionId "My Subscription Id" -Location "West US 3"
.\test\resources\create-resource-group.ps1 -SubscriptionName "My Subscription" -Location "West US 3"
```
Then, whenever you want to create/update the resources, you can run the `deploy.ps1` script in the `resources` directory. As long as you're using the default naming convention (`[username]-e2e-testing`), you just need to specify the Subscription. For example:
```powershell
.\test\resources\deploy.ps1 -Subscription "My Subscription"
.\test\resources\deploy.ps1 -SubscriptionName "My Subscription"
```
You'll get a confirmation prompt before anything is deployed:

View File

@@ -1,50 +1,114 @@
import { expect, test } from "@playwright/test";
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 }) => {
const keyspaceId = generateUniqueName("db");
const tableId = "testtable"; // A unique table name isn't needed because the keyspace is unique
test("Cassandra: Keyspace and table CRUD", async ({ page }) => {
const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
await explorer.globalCommandButton("New Table").click();
await explorer.whilePanelOpen(
"Add Table",
async (panel, okButton) => {
await panel.getByPlaceholder("Type a new keyspace id").fill(keyspaceId);
await panel.getByPlaceholder("Enter table Id").fill(tableId);
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
// Create
await openAndFillCreateCassandraTablePanel(explorer, {
keyspaceId,
tableId,
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const keyspaceNode = await explorer.waitForNode(keyspaceId);
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 tableNode = await explorer.waitForContainerNode(keyspaceId, tableId);
await tableNode.openContextMenu();
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 },
);
await expect(tableNode.element).toBeAttached();
// 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();
});

View File

@@ -1,7 +1,6 @@
import { DefaultAzureCredential } from "@azure/identity";
import { Frame, Locator, Page, expect } from "@playwright/test";
import crypto from "crypto";
import { TestContainerContext, TestDatabaseContext } from "./testData";
const RETRY_COUNT = 3;
@@ -56,11 +55,6 @@ export const defaultAccounts: Record<TestAccount, string> = {
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c";
export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000;
export const TEST_MANUAL_THROUGHPUT_RU = 800;
export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000;
export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000;
export const TEST_MANUAL_THROUGHPUT_RU_2K = 2000;
export const ONE_MINUTE_MS: number = 60 * 1000;
function tryGetStandardName(accountType: TestAccount) {
if (process.env.DE_TEST_ACCOUNT_PREFIX) {
@@ -325,11 +319,6 @@ type PanelOpenOptions = {
closeTimeout?: number;
};
export enum CommandBarButton {
Save = "Save",
ExecuteQuery = "Execute Query",
}
/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */
export class DataExplorer {
constructor(public frame: Frame) {}
@@ -355,12 +344,12 @@ 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.
*/
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 */
commandBarButton(commandBarButton: CommandBarButton): Locator {
return this.frame.getByTestId(`CommandBar/Button:${commandBarButton}`).and(this.frame.locator("css=button"));
commandBarButton(label: string): Locator {
return this.frame.getByTestId(`CommandBar/Button:${label}`).and(this.frame.locator("css=button"));
}
dialogButton(label: string): Locator {
@@ -456,22 +445,6 @@ export class DataExplorer {
await panel.waitFor({ state: "detached", timeout: options.closeTimeout });
}
/** Opens the Scale & Settings panel for the specified container */
async openScaleAndSettings(context: TestContainerContext): Promise<void> {
const containerNode = await this.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
const scaleAndSettingsButton = this.frame.getByTestId(
`TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`,
);
await scaleAndSettingsButton.click();
}
/** Gets the console message element */
getConsoleMessage(): Locator {
return this.frame.getByTestId("notification-console/header-status");
}
/** Waits for the Data Explorer app to load */
static async waitForExplorer(page: Page) {
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();

View File

@@ -1,22 +1,108 @@
import { expect, test } from "@playwright/test";
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 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);
// Create new database and graph
await explorer.globalCommandButton("New Graph").click();
await explorer.whilePanelOpen(
"New Graph",
async (panel, okButton) => {
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Graph id, Example Graph1" }).fill(graphId);
await panel.getByTestId("AddCollectionPanel/DatabaseId").fill(databaseId);
await panel.getByTestId("AddCollectionPanel/CollectionId").fill(graphId);
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();
},
{ closeTimeout: 5 * 60 * 1000 },
@@ -24,29 +110,9 @@ test("Gremlin graph CRUD", async ({ page }) => {
const databaseNode = await explorer.waitForNode(databaseId);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await expect(graphNode.element).toBeAttached();
await graphNode.openContextMenu();
await graphNode.contextMenuItem("Delete Graph").click();
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 },
);
// Cleanup
await deleteDatabase(explorer, databaseId);
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 { 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],
["3.2 API", TestAccount.Mongo32],
] as [string, TestAccount][]
).forEach(([apiVersionDescription, accountType]) => {
test(`Mongo CRUD using ${apiVersionDescription}`, async ({ page }) => {
["latest API version", MONGO_CONFIG],
["3.2 API", MONGO32_CONFIG],
] as [string, typeof MONGO_CONFIG][]
).forEach(([apiVersionDescription, config]) => {
test(`Mongo: Database and collection CRUD using ${apiVersionDescription}`, async ({ page }) => {
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();
await explorer.whilePanelOpen(
"New Collection",
async (panel, okButton) => {
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Collection id, Example Collection1" }).fill(collectionId);
await panel.getByRole("textbox", { name: "Shard key" }).fill("pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
// Create
await openAndFillCreateContainerPanel(explorer, config, {
databaseId,
containerId: collectionId,
partitionKey: "pk",
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const databaseNode = await explorer.waitForNode(databaseId);
const collectionNode = await explorer.waitForContainerNode(databaseId, collectionId);
await expect(collectionNode.element).toBeAttached();
await collectionNode.openContextMenu();
await collectionNode.contextMenuItem("Delete Collection").click();
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 },
);
// Delete collection
await deleteContainer(explorer, databaseId, collectionId, "Delete Collection");
await expect(collectionNode.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 },
);
// Delete database
await deleteDatabase(explorer, databaseId);
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 { 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 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);
await explorer.globalCommandButton("New Container").click();
await explorer.whilePanelOpen(
"New Container",
async (panel, okButton) => {
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId);
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
// Create
await openAndFillCreateContainerPanel(explorer, SQL_CONFIG, {
databaseId,
containerId,
partitionKey: "/pk",
isAutoscale: true,
throughputValue: TEST_AUTOSCALE_THROUGHPUT_RU,
});
const databaseNode = await explorer.waitForNode(databaseId);
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 containerNode = await explorer.waitForContainerNode(databaseId, containerId);
await containerNode.openContextMenu();
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 },
);
await expect(containerNode.element).toBeAttached();
// 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();
});

View File

@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { CommandBarButton, DataExplorer, Editor, QueryTab, TestAccount } from "../fx";
import { DataExplorer, Editor, QueryTab, TestAccount } from "../fx";
import { TestContainerContext, TestItem, createTestSQLContainer } from "../testData";
let context: TestContainerContext = null!;
@@ -37,7 +37,7 @@ test.afterAll("Delete Test Database", async () => {
test("Query results", async () => {
// Run the query and verify the results
await queryEditor.locator.click();
const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
const executeQueryButton = explorer.commandBarButton("Execute Query");
await executeQueryButton.click();
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
@@ -59,7 +59,7 @@ test("Query results", async () => {
test("Query stats", async () => {
// Run the query and verify the results
await queryEditor.locator.click();
const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
const executeQueryButton = explorer.commandBarButton("Execute Query");
await executeQueryButton.click();
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
@@ -77,7 +77,7 @@ test("Query errors", async () => {
await queryEditor.setText("SELECT\n glarb(c.id),\n blarg(c.id)\nFROM c");
// Run the query and verify the results
const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
const executeQueryButton = explorer.commandBarButton("Execute Query");
await executeQueryButton.click();
await expect(queryTab.errorList).toBeAttached({ timeout: 60 * 1000 });

View File

@@ -1,129 +0,0 @@
import { expect, Locator, test } from "@playwright/test";
import {
CommandBarButton,
DataExplorer,
ONE_MINUTE_MS,
TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K,
TEST_MANUAL_THROUGHPUT_RU_2K,
TestAccount,
} from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Autoscale and Manual throughput", () => {
let context: TestContainerContext = null!;
let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true);
});
test.beforeEach("Open container settings", async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQL);
// Click Scale & Settings and open Scale tab
await explorer.openScaleAndSettings(context);
const scaleTab = explorer.frame.getByTestId("settings-tab-header/ScaleTab");
await scaleTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
test("Update autoscale max throughput", async () => {
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
await switchManualToAutoscaleThroughput();
// Update autoscale max throughput
await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K.toString());
// Save
await explorer.commandBarButton(CommandBarButton.Save).click();
// Read console message
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: 2 * ONE_MINUTE_MS,
},
);
});
test("Update autoscale max throughput passed allowed limit", async () => {
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
await switchManualToAutoscaleThroughput();
// Get soft allowed max throughput and remove commas
const softAllowedMaxThroughputString = await explorer.frame
.getByTestId("soft-allowed-maximum-throughput")
.innerText();
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
// Try to set autoscale max throughput above allowed limit
await getThroughputInput("autopilot").fill((softAllowedMaxThroughput * 10).toString());
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
"This update isn't possible because it would increase the total throughput",
);
});
test("Update autoscale max throughput with invalid increment", async () => {
// By default the created container has manual throughput (Containers created via JS SDK v4.7.0 cannot be created with autoscale throughput)
await switchManualToAutoscaleThroughput();
// Try to set autoscale max throughput with invalid increment
await getThroughputInput("autopilot").fill("1100");
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("autopilot")).toContainText(
"Throughput value must be in increments of 1000",
);
});
test("Update manual throughput", async () => {
await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU_2K.toString());
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: 2 * ONE_MINUTE_MS,
},
);
});
test("Update manual throughput passed allowed limit", async () => {
// Get soft allowed max throughput and remove commas
const softAllowedMaxThroughputString = await explorer.frame
.getByTestId("soft-allowed-maximum-throughput")
.innerText();
const softAllowedMaxThroughput = Number(softAllowedMaxThroughputString.replace(/,/g, ""));
// Try to set manual throughput above allowed limit
await getThroughputInput("manual").fill((softAllowedMaxThroughput * 10).toString());
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeDisabled();
await expect(getThroughputInputErrorMessage("manual")).toContainText(
"This update isn't possible because it would increase the total throughput",
);
});
// Helper methods
const getThroughputInput = (type: "manual" | "autopilot"): Locator => {
return explorer.frame.getByTestId(`${type}-throughput-input`);
};
const getThroughputInputErrorMessage = (type: "manual" | "autopilot"): Locator => {
return explorer.frame.getByTestId(`${type}-throughput-input-error`);
};
const switchManualToAutoscaleThroughput = async (): Promise<void> => {
const autoscaleRadioButton = explorer.frame.getByText("Autoscale", { exact: true });
await autoscaleRadioButton.click();
await expect(explorer.commandBarButton(CommandBarButton.Save)).toBeEnabled();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for collection ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
};
});

View File

@@ -1,70 +0,0 @@
import { expect, test } from "@playwright/test";
import { CommandBarButton, DataExplorer, ONE_MINUTE_MS, TestAccount } from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Settings under Scale & Settings", () => {
let context: TestContainerContext = null!;
let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true);
});
test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQL);
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
// Click Scale & Settings and open Scale tab
await explorer.openScaleAndSettings(context);
const settingsTab = explorer.frame.getByTestId("settings-tab-header/SubSettingsTab");
await settingsTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
test("Update TTL to On (no default)", async () => {
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test("Update TTL to On (with user entry)", async () => {
const ttlOnRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-option" });
await ttlOnRadioButton.click();
// Enter TTL seconds
const ttlInput = explorer.frame.getByTestId("ttl-input");
await ttlInput.fill("30000");
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test("Update TTL to Off", async () => {
// By default TTL is set to off so we need to first set it to On
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
// Set it to Off
const ttlOffRadioButton = explorer.frame.getByRole("radio", { name: "ttl-off-option" });
await ttlOffRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
});

View File

@@ -1,288 +0,0 @@
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { CosmosClient, CosmosClientOptions, Database } from "@azure/cosmos";
import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
import { Locator, expect, test } from "@playwright/test";
import {
CommandBarButton,
DataExplorer,
ONE_MINUTE_MS,
TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K,
TEST_MANUAL_THROUGHPUT_RU,
TestAccount,
generateUniqueName,
getAccountName,
getAzureCLICredentials,
resourceGroupName,
subscriptionId,
} from "../../fx";
// Helper class for database context
class TestDatabaseContext {
constructor(
public armClient: CosmosDBManagementClient,
public client: CosmosClient,
public database: Database,
) {}
async dispose() {
await this.database.delete();
}
}
// Options for creating test database
interface CreateTestDBOptions {
throughput?: number;
maxThroughput?: number; // For autoscale
}
// Helper function to create a test database with shared throughput
async function createTestDB(options?: CreateTestDBOptions): Promise<TestDatabaseContext> {
const databaseId = generateUniqueName("db");
const credentials = getAzureCLICredentials();
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
const accountName = getAccountName(TestAccount.SQL);
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const clientOptions: CosmosClientOptions = {
endpoint: account.documentEndpoint!,
};
const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
if (nosqlAccountRbacToken) {
clientOptions.tokenProvider = async (): Promise<string> => {
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
const authorizationToken = `${AUTH_PREFIX}${nosqlAccountRbacToken}`;
return authorizationToken;
};
} else {
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
clientOptions.key = keys.primaryMasterKey;
}
const client = new CosmosClient(clientOptions);
// Create database with provisioned throughput (shared throughput)
// This checks the "Provision database throughput" option
const { database } = await client.databases.create({
id: databaseId,
throughput: options?.throughput, // Manual throughput (e.g., 400)
maxThroughput: options?.maxThroughput, // Autoscale max throughput (e.g., 1000)
});
return new TestDatabaseContext(armClient, client, database);
}
test.describe("Database with Shared Throughput", () => {
let dbContext: TestDatabaseContext = null!;
let explorer: DataExplorer = null!;
const containerId = "sharedcontainer";
// Helper methods
const getThroughputInput = (type: "manual" | "autopilot"): Locator => {
return explorer.frame.getByTestId(`${type}-throughput-input`);
};
test.afterEach(async () => {
// Clean up: delete the created database
await dbContext?.dispose();
});
test.describe("Manual Throughput Tests", () => {
test.beforeEach(async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQL);
});
test("Create database with shared manual throughput and verify Scale node in UI", async () => {
test.setTimeout(120000); // 2 minutes timeout
// Create database with shared manual throughput (400 RU/s)
dbContext = await createTestDB({ throughput: 400 });
// Verify database node appears in the tree
const databaseNode = await explorer.waitForNode(dbContext.database.id);
expect(databaseNode).toBeDefined();
// Expand the database node to see child nodes
await databaseNode.expand();
// Verify that "Scale" node appears under the database
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
expect(scaleNode).toBeDefined();
await expect(scaleNode.element).toBeVisible();
});
test("Add container to shared database without dedicated throughput", async () => {
// Create database with shared manual throughput
dbContext = await createTestDB({ throughput: 400 });
// Wait for the database to appear in the tree
await explorer.waitForNode(dbContext.database.id);
// Add a container to the shared database via UI
await explorer.globalCommandButton("New Container").click();
await explorer.whilePanelOpen(
"New Container",
async (panel, okButton) => {
// Select "Use existing" database
const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i });
await useExistingRadio.click();
// Select the database from dropdown using the new data-testid
const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" });
await databaseDropdown.click();
await explorer.frame.getByRole("option", { name: dbContext.database.id }).click();
// Now you can target the specific database option by its data-testid
//await panel.getByTestId(`database-option-${dbContext.database.id}`).click();
// Fill container id
await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId);
// Fill partition key
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
// Ensure "Provision dedicated throughput" is NOT checked
const dedicatedThroughputCheckbox = panel.getByRole("checkbox", {
name: /Provision dedicated throughput for this container/i,
});
if (await dedicatedThroughputCheckbox.isVisible()) {
const isChecked = await dedicatedThroughputCheckbox.isChecked();
if (isChecked) {
await dedicatedThroughputCheckbox.uncheck();
}
}
await okButton.click();
},
{ closeTimeout: 5 * ONE_MINUTE_MS },
);
// Verify container was created under the database
const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId);
expect(containerNode).toBeDefined();
});
test("Scale shared database manual throughput", async () => {
// Create database with shared manual throughput (400 RU/s)
dbContext = await createTestDB({ throughput: 400 });
// Navigate to the scale settings by clicking the "Scale" node in the tree
const databaseNode = await explorer.waitForNode(dbContext.database.id);
await databaseNode.expand();
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
await scaleNode.element.click();
// Update manual throughput from 400 to 800
await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString());
// Save changes
await explorer.commandBarButton(CommandBarButton.Save).click();
// Verify success message
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for database ${dbContext.database.id}`,
{ timeout: 2 * ONE_MINUTE_MS },
);
});
test("Scale shared database from manual to autoscale", async () => {
// Create database with shared manual throughput (400 RU/s)
dbContext = await createTestDB({ throughput: 400 });
// Open database settings by clicking the "Scale" node
const databaseNode = await explorer.waitForNode(dbContext.database.id);
await databaseNode.expand();
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
await scaleNode.element.click();
// Switch to Autoscale
const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true });
await autoscaleRadio.click();
// Set autoscale max throughput to 1000
//await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
// Save changes
await explorer.commandBarButton(CommandBarButton.Save).click();
// Verify success message
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for database ${dbContext.database.id}`,
{ timeout: 2 * ONE_MINUTE_MS },
);
});
});
test.describe("Autoscale Throughput Tests", () => {
test.beforeEach(async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQL);
});
test("Create database with shared autoscale throughput and verify in UI", async () => {
test.setTimeout(120000); // 2 minutes timeout
// Create database with shared autoscale throughput (max 1000 RU/s)
dbContext = await createTestDB({ maxThroughput: 1000 });
// Verify database node appears
const databaseNode = await explorer.waitForNode(dbContext.database.id);
expect(databaseNode).toBeDefined();
// Expand the database node to see child nodes
await databaseNode.expand();
// Verify that "Scale" node appears under the database
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
expect(scaleNode).toBeDefined();
await expect(scaleNode.element).toBeVisible();
});
test("Scale shared database autoscale throughput", async () => {
// Create database with shared autoscale throughput (max 1000 RU/s)
dbContext = await createTestDB({ maxThroughput: 1000 });
// Open database settings
const databaseNode = await explorer.waitForNode(dbContext.database.id);
await databaseNode.expand();
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
await scaleNode.element.click();
// Update autoscale max throughput from 1000 to 4000
await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString());
// Save changes
await explorer.commandBarButton(CommandBarButton.Save).click();
// Verify success message
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for database ${dbContext.database.id}`,
{ timeout: 2 * ONE_MINUTE_MS },
);
});
test("Scale shared database from autoscale to manual", async () => {
// Create database with shared autoscale throughput (max 1000 RU/s)
dbContext = await createTestDB({ maxThroughput: 1000 });
// Open database settings
const databaseNode = await explorer.waitForNode(dbContext.database.id);
await databaseNode.expand();
const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`);
await scaleNode.element.click();
// Switch to Manual
const manualRadio = explorer.frame.getByText("Manual", { exact: true });
await manualRadio.click();
// Save changes
await explorer.commandBarButton(CommandBarButton.Save).click();
// Verify success message
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated offer for database ${dbContext.database.id}`,
{ timeout: 2 * ONE_MINUTE_MS },
);
});
});
});

View File

@@ -1,35 +1,116 @@
import { expect, test } from "@playwright/test";
import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUniqueName } from "../fx";
import { TABLES_CONFIG, deleteContainer, openAndFillCreateContainerPanel } from "../helpers/containerCreationHelpers";
test("Tables CRUD", async ({ page }) => {
const tableId = generateUniqueName("table"); // A unique table name IS needed because the database is shared when using Table Storage.
test("Tables: CRUD", async ({ page }) => {
const tableId = generateUniqueName("table");
const explorer = await DataExplorer.open(page, TestAccount.Tables);
await explorer.globalCommandButton("New Table").click();
await explorer.whilePanelOpen(
"New Table",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Table id, Example Table1" }).fill(tableId);
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
// Create
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();
await tableNode.openContextMenu();
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 },
);
// Delete table
await deleteContainer(explorer, "TablesDB", tableId, "Delete Table");
await expect(tableNode.element).not.toBeAttached();
});
test("Tables: New database shared throughput", async ({ page }) => {
const tableId = generateUniqueName("table");
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();
});

View File

@@ -74,60 +74,6 @@ export class TestContainerContext {
}
}
export class TestDatabaseContext {
constructor(
public armClient: CosmosDBManagementClient,
public client: CosmosClient,
public database: Database,
) {}
async dispose() {
await this.database.delete();
}
}
export interface CreateTestDBOptions {
throughput?: number;
maxThroughput?: number; // For autoscale
}
export async function createTestDB(options?: CreateTestDBOptions): Promise<TestDatabaseContext> {
const databaseId = generateUniqueName("db");
const credentials = getAzureCLICredentials();
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
const accountName = getAccountName(TestAccount.SQL);
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const clientOptions: CosmosClientOptions = {
endpoint: account.documentEndpoint!,
};
const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
if (nosqlAccountRbacToken) {
clientOptions.tokenProvider = async (): Promise<string> => {
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
const authorizationToken = `${AUTH_PREFIX}${nosqlAccountRbacToken}`;
return authorizationToken;
};
} else {
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
clientOptions.key = keys.primaryMasterKey;
}
const client = new CosmosClient(clientOptions);
// Create database with provisioned throughput (shared throughput)
// This checks the "Provision database throughput" option
const { database } = await client.databases.create({
id: databaseId,
throughput: options?.throughput, // Manual throughput (e.g., 400)
maxThroughput: options?.maxThroughput, // Autoscale max throughput (e.g., 1000)
});
return new TestDatabaseContext(armClient, client, database);
}
export async function createTestSQLContainer(includeTestData?: boolean) {
const databaseId = generateUniqueName("db");
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique

View File

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