Compare commits

..

15 Commits

Author SHA1 Message Date
Asier Isayas
3ea5a059b0 fix regex 2025-12-19 10:26:53 -08:00
Asier Isayas
37a5663bad format 2025-12-17 11:35:28 -08:00
Asier Isayas
31c9574df4 cleanup 2025-12-17 11:32:55 -08:00
Asier Isayas
2e8e02f75b nit 2025-12-17 11:24:42 -08:00
Asier Isayas
8809ba6cd2 fix tests 2025-12-17 11:21:39 -08:00
Asier Isayas
592f2cd7e6 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas-playwright-2 2025-12-17 11:09:08 -08:00
Asier Isayas
d966f4f2a7 Add more playwright tests 2025-12-17 11:03:28 -08:00
Asier Isayas
4801aae754 changed throughput above limit 2025-12-08 11:21:20 -08:00
Asier Isayas
2a02112d87 fix autoscale selector 2025-12-08 09:55:21 -08:00
Asier Isayas
bbfff77495 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/playwright 2025-12-08 07:39:33 -08:00
Asier Isayas
f695b42071 fix tests 2025-12-04 12:24:47 -08:00
Asier Isayas
a8a96e22b4 fix unit tests 2025-12-04 12:05:47 -08:00
Asier Isayas
a1b026544d fix unit tests and lint 2025-12-04 11:52:31 -08:00
Asier Isayas
a912233b33 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/playwright 2025-12-04 11:43:17 -08:00
Asier Isayas
487130f6e3 Add playwright tests for Autoscale/Manual Throughpout and TTL 2025-12-04 11:40:48 -08:00
50 changed files with 368 additions and 1235 deletions

View File

@@ -192,9 +192,6 @@ jobs:
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV

View File

@@ -13,7 +13,6 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) =>
return (
<Overlay
data-test="loading-overlay"
styles={{
root: {
backgroundColor: "rgba(255,255,255,0.9)",

View File

@@ -3,7 +3,6 @@
exports[`LoadingOverlay should handle long labels properly 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
@@ -23,7 +22,6 @@ exports[`LoadingOverlay should handle long labels properly 1`] = `
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
@@ -43,7 +41,6 @@ exports[`LoadingOverlay should render loading overlay when isLoading is true 1`]
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
@@ -63,7 +60,6 @@ exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"

View File

@@ -35,7 +35,6 @@ const AddManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<InfoTooltip content={managedIdentityTooltip} />
</Text>
<Toggle
data-test="btn-toggle"
checked={systemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -65,7 +65,6 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
<InfoTooltip content={TooltipContent} />
</Text>
<Toggle
data-test="btn-toggle"
checked={readPermissionAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -12,7 +12,7 @@ import { useCopyJobPrerequisitesCache } from "../../Utils/useCopyJobPrerequisite
import usePermissionSections, { PermissionGroupConfig, PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component, completed, disabled }) => (
<AccordionItem key={id} value={id} disabled={disabled} data-test="accordion-item">
<AccordionItem key={id} value={id} disabled={disabled}>
<AccordionHeader className="accordionHeader">
<Text className="accordionHeaderText" variant="medium">
{title}
@@ -25,13 +25,13 @@ const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Compo
height={completed ? 20 : 24}
/>
</AccordionHeader>
<AccordionPanel aria-disabled={disabled} className="accordionPanel" data-test="accordion-panel">
<AccordionPanel aria-disabled={disabled} className="accordionPanel">
<Component />
</AccordionPanel>
</AccordionItem>
);
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, description, sections }) => {
const PermissionGroup: React.FC<PermissionGroupConfig> = ({ title, description, sections }) => {
const [openItems, setOpenItems] = React.useState<string[]>([]);
useEffect(() => {
@@ -44,7 +44,6 @@ const PermissionGroup: React.FC<PermissionGroupConfig> = ({ id, title, descripti
return (
<Stack
data-test={`permission-group-container-${id}`}
tokens={{ childrenGap: 15 }}
styles={{
root: {
@@ -100,11 +99,7 @@ const AssignPermissions = () => {
}, []);
return (
<Stack
data-test="Panel:AssignPermissionsContainer"
className="assignPermissionsContainer"
tokens={{ childrenGap: 20 }}
>
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 20 }}>
<Text variant="medium">
{isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online
? ContainerCopyMessages.assignPermissions.intraAccountOnlineDescription(

View File

@@ -31,7 +31,6 @@ const DefaultManagedIdentity: React.FC<AddManagedIdentityProps> = () => {
<InfoTooltip content={managedIdentityTooltip} />
</div>
<Toggle
data-test="btn-toggle"
checked={defaultSystemAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}

View File

@@ -127,7 +127,6 @@ const PointInTimeRestore: React.FC = () => {
<Stack.Item>
{showRefreshButton ? (
<PrimaryButton
data-test="pointInTimeRestore:RefreshBtn"
className="fullWidth"
text={ContainerCopyMessages.refreshButtonLabel}
iconProps={{ iconName: "Refresh" }}
@@ -135,7 +134,6 @@ const PointInTimeRestore: React.FC = () => {
/>
) : (
<PrimaryButton
data-test="pointInTimeRestore:PrimaryBtn"
className="fullWidth"
text={loading ? "" : ContainerCopyMessages.pointInTimeRestore.buttonText}
{...(loading ? { iconProps: { iconName: "SyncStatusSolid" } } : {})}

View File

@@ -67,7 +67,6 @@ exports[`AddManagedIdentity Snapshot Tests renders initial state correctly 1`] =
class="ms-Toggle-background pill-117"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle1"
role="switch"
type="button"
@@ -155,7 +154,6 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
class="ms-Toggle-background pill-121"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle11"
role="switch"
type="button"
@@ -175,12 +173,10 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = `
</div>
<div
class="ms-Stack popover-container foreground loading css-123"
data-test="popover-container"
style="max-width: 450px;"
>
<div
class="ms-Overlay root-135"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-137"
@@ -327,7 +323,6 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
class="ms-Toggle-background pill-121"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle3"
role="switch"
type="button"
@@ -347,7 +342,6 @@ exports[`AddManagedIdentity Snapshot Tests renders with toggle on and popover vi
</div>
<div
class="ms-Stack popover-container foreground css-123"
data-test="popover-container"
style="max-width: 450px;"
>
<span

View File

@@ -41,7 +41,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle17"
role="switch"
type="button"
@@ -104,7 +103,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle16"
role="switch"
type="button"
@@ -167,7 +165,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle3"
role="switch"
type="button"
@@ -230,7 +227,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle1"
role="switch"
type="button"
@@ -318,7 +314,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle0"
role="switch"
type="button"
@@ -381,7 +376,6 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle2"
role="switch"
type="button"

View File

@@ -4,7 +4,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -16,7 +15,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
>
<div
class="ms-Stack css-113"
@@ -38,7 +36,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -88,7 +85,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -138,7 +134,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div>
Incomplete Component
@@ -147,7 +142,6 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -207,7 +201,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -219,7 +212,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
>
<div
class="ms-Stack css-113"
@@ -241,7 +233,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -291,7 +282,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -341,7 +331,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div>
Incomplete Component
@@ -350,7 +339,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -410,7 +398,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -422,7 +409,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
>
<div
class="ms-Stack css-113"
@@ -444,7 +430,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -494,7 +479,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -544,7 +528,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div>
Incomplete Component
@@ -553,7 +536,6 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -613,7 +595,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -625,7 +606,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-testGroup"
>
<div
class="ms-Stack css-113"
@@ -647,7 +627,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -697,7 +676,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -747,7 +725,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div>
Incomplete Component
@@ -756,7 +733,6 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___lyghz50_53x5ri0 f1s2aq7o f1c21dwh f1s184ao ft85np5 fwrgwhw"
@@ -816,7 +792,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -828,7 +803,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-crossAccountConfigs"
>
<div
class="ms-Stack css-113"
@@ -850,7 +824,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -902,7 +875,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
</div>
<div
class="ms-Stack css-112"
data-test="permission-group-container-onlineConfigs"
>
<div
class="ms-Stack css-113"
@@ -924,7 +896,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -974,7 +945,6 @@ exports[`AssignPermissions Component Permission Groups should render multiple pe
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div
data-testid="online-copy-enabled"
@@ -994,7 +964,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -1006,7 +975,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-onlineConfigs"
>
<div
class="ms-Stack css-113"
@@ -1028,7 +996,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1078,7 +1045,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1128,7 +1094,6 @@ exports[`AssignPermissions Component Permission Groups should render online migr
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div
data-testid="online-copy-enabled"
@@ -1148,7 +1113,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -1160,7 +1124,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
>
<div
class="ms-Stack css-112"
data-test="permission-group-container-crossAccountConfigs"
>
<div
class="ms-Stack css-113"
@@ -1182,7 +1145,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1232,7 +1194,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
</div>
<div
class="fui-AccordionItem"
data-test="accordion-item"
>
<div
class="fui-AccordionHeader accordionHeader ___kex8dp0_1udlp87 f19n0e5 f1c21dwh f1s184ao ft85np5"
@@ -1282,7 +1243,6 @@ exports[`AssignPermissions Component Permission Groups should render permission
<div
aria-disabled="false"
class="fui-AccordionPanel accordionPanel ___1rufncu_1hx1scr f1axvtxu"
data-test="accordion-panel"
>
<div
data-testid="add-read-permission"
@@ -1302,7 +1262,6 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"
@@ -1324,7 +1283,6 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<div>
<div
class="ms-Stack assignPermissionsContainer css-109"
data-test="Panel:AssignPermissionsContainer"
>
<span
class="css-110"

View File

@@ -41,7 +41,6 @@ exports[`DefaultManagedIdentity Edge Cases should handle missing account name gr
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle14"
role="switch"
type="button"
@@ -104,7 +103,6 @@ exports[`DefaultManagedIdentity Edge Cases should handle null account 1`] = `
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle15"
role="switch"
type="button"
@@ -167,7 +165,6 @@ exports[`DefaultManagedIdentity Loading States should render loading state snaps
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle10"
role="switch"
type="button"
@@ -259,7 +256,6 @@ exports[`DefaultManagedIdentity Rendering should render correctly with default s
class="ms-Toggle-background pill-115"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle0"
role="switch"
type="button"
@@ -322,7 +318,6 @@ exports[`DefaultManagedIdentity Toggle Interactions should render toggle with ch
class="ms-Toggle-background pill-119"
data-is-focusable="true"
data-ktp-target="true"
data-test="btn-toggle"
id="Toggle7"
role="switch"
type="button"

View File

@@ -56,7 +56,6 @@ exports[`PointInTimeRestore Initial Render should render correctly with default
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
data-test="pointInTimeRestore:PrimaryBtn"
type="button"
>
<span
@@ -132,7 +131,6 @@ exports[`PointInTimeRestore Initial Render should render with empty account name
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
data-test="pointInTimeRestore:PrimaryBtn"
type="button"
>
<span
@@ -163,7 +161,6 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
>
<div
class="ms-Overlay root-123"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-125"
@@ -226,7 +223,6 @@ exports[`PointInTimeRestore Snapshots should match snapshot in loading state 1`]
aria-disabled="true"
class="ms-Button ms-Button--primary is-disabled fullWidth root-128"
data-is-focusable="false"
data-test="pointInTimeRestore:PrimaryBtn"
disabled=""
type="button"
>
@@ -305,7 +301,6 @@ exports[`PointInTimeRestore Snapshots should match snapshot with refresh button
<button
class="ms-Button ms-Button--primary fullWidth root-115"
data-is-focusable="true"
data-test="pointInTimeRestore:RefreshBtn"
type="button"
>
<span

View File

@@ -19,21 +19,9 @@ const NavigationControls: React.FC<NavigationControlsProps> = ({
isPreviousDisabled,
}) => (
<Stack horizontal tokens={{ childrenGap: 20 }}>
<PrimaryButton
data-test="copy-job-primary"
text={primaryBtnText}
onClick={onPrimary}
allowDisabledFocus
disabled={isPrimaryDisabled}
/>
<DefaultButton
data-test="copy-job-previous"
text="Previous"
onClick={onPrevious}
allowDisabledFocus
disabled={isPreviousDisabled}
/>
<DefaultButton data-test="copy-job-cancel" text="Cancel" onClick={onCancel} />
<PrimaryButton text={primaryBtnText} onClick={onPrimary} allowDisabledFocus disabled={isPrimaryDisabled} />
<DefaultButton text="Previous" onClick={onPrevious} allowDisabledFocus disabled={isPreviousDisabled} />
<DefaultButton text="Cancel" onClick={onCancel} />
</Stack>
);

View File

@@ -17,7 +17,6 @@ const PopoverContainer: React.FC<PopoverContainerProps> = React.memo(
({ isLoading = false, title, children, onPrimary, onCancel }) => {
return (
<Stack
data-test="popover-container"
className={`popover-container foreground ${isLoading ? "loading" : ""}`}
tokens={{ childrenGap: 20 }}
style={{ maxWidth: 450 }}

View File

@@ -4,7 +4,6 @@ exports[`PopoverMessage Component Edge Cases should handle empty string title 1`
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -72,7 +71,6 @@ exports[`PopoverMessage Component Edge Cases should handle null children 1`] = `
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -135,7 +133,6 @@ exports[`PopoverMessage Component Edge Cases should handle undefined children 1`
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -198,7 +195,6 @@ exports[`PopoverMessage Component Edge Cases should handle very long title 1`] =
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -270,7 +266,6 @@ exports[`PopoverMessage Component Rendering should render correctly when visible
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -340,7 +335,6 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -415,7 +409,6 @@ exports[`PopoverMessage Component Rendering should render correctly with differe
<div>
<div
class="ms-Stack popover-container foreground css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<span
@@ -485,7 +478,6 @@ exports[`PopoverMessage Component Rendering should render correctly with loading
<div>
<div
class="ms-Stack popover-container foreground loading css-109"
data-test="popover-container"
style="max-width: 450px;"
>
<div

View File

@@ -22,7 +22,6 @@ const CreateCopyJobScreens: React.FC = () => {
<Stack.Item className="createCopyJobScreensContent">
{contextError && (
<MessageBar
data-test="Panel:ErrorContainer"
className="createCopyJobErrorMessageBar"
messageBarType={MessageBarType.blocked}
isMultiline={false}

View File

@@ -31,17 +31,17 @@ const PreviewCopyJob: React.FC = () => {
}));
};
return (
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer" data-test="Panel:PreviewCopyJob">
<Stack tokens={{ childrenGap: 20 }} className="previewCopyJobContainer">
<FieldRow label={ContainerCopyMessages.jobNameLabel}>
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
<TextField value={jobName} onChange={onJobNameChange} />
</FieldRow>
<Stack>
<Text className="bold">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
<Text data-test="source-subscription-name">{copyJobState.source?.subscription?.displayName}</Text>
<Text>{copyJobState.source?.subscription?.displayName}</Text>
</Stack>
<Stack>
<Text className="bold">{ContainerCopyMessages.sourceAccountLabel}</Text>
<Text data-test="source-account-name">{copyJobState.source?.account?.name}</Text>
<Text>{copyJobState.source?.account?.name}</Text>
</Stack>
<Stack>
<DetailsList

View File

@@ -3,7 +3,6 @@
exports[`PreviewCopyJob should handle special characters in database and container names 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -33,7 +32,6 @@ exports[`PreviewCopyJob should handle special characters in database and contain
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField84"
type="text"
value="job-with@special#chars_123"
@@ -53,7 +51,6 @@ exports[`PreviewCopyJob should handle special characters in database and contain
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -68,7 +65,6 @@ exports[`PreviewCopyJob should handle special characters in database and contain
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -325,7 +321,6 @@ exports[`PreviewCopyJob should handle special characters in database and contain
exports[`PreviewCopyJob should render component with cross-subscription setup 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -355,7 +350,6 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField96"
type="text"
value=""
@@ -375,7 +369,6 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -390,7 +383,6 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -647,7 +639,6 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
exports[`PreviewCopyJob should render with default state and empty job name 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -677,7 +668,6 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField0"
type="text"
value=""
@@ -697,7 +687,6 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -712,7 +701,6 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -969,7 +957,6 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
exports[`PreviewCopyJob should render with long subscription and account names 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -999,7 +986,6 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField60"
type="text"
value=""
@@ -1019,7 +1005,6 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
This is a very long subscription name that might cause display issues if not handled properly
</span>
@@ -1034,7 +1019,6 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
</span>
<span
class="css-125"
data-test="source-account-name"
>
this-is-a-very-long-database-account-name-that-might-cause-display-issues
</span>
@@ -1291,7 +1275,6 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
exports[`PreviewCopyJob should render with missing source account information 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -1321,7 +1304,6 @@ exports[`PreviewCopyJob should render with missing source account information 1`
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField36"
type="text"
value=""
@@ -1341,7 +1323,6 @@ exports[`PreviewCopyJob should render with missing source account information 1`
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -1607,7 +1588,6 @@ exports[`PreviewCopyJob should render with missing source account information 1`
exports[`PreviewCopyJob should render with missing source subscription information 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -1637,7 +1617,6 @@ exports[`PreviewCopyJob should render with missing source subscription informati
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField24"
type="text"
value=""
@@ -1666,7 +1645,6 @@ exports[`PreviewCopyJob should render with missing source subscription informati
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -1923,7 +1901,6 @@ exports[`PreviewCopyJob should render with missing source subscription informati
exports[`PreviewCopyJob should render with online migration type 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -1953,7 +1930,6 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField72"
type="text"
value="online-migration-job"
@@ -1973,7 +1949,6 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -1988,7 +1963,6 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -2245,7 +2219,6 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -2275,7 +2248,6 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField12"
type="text"
value="custom-job-name-123"
@@ -2295,7 +2267,6 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -2310,7 +2281,6 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>
@@ -2567,7 +2537,6 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
exports[`PreviewCopyJob should render with undefined database and container names 1`] = `
<div
class="ms-Stack previewCopyJobContainer css-109"
data-test="Panel:PreviewCopyJob"
>
<div
class="ms-Stack flex-row css-110"
@@ -2597,7 +2566,6 @@ exports[`PreviewCopyJob should render with undefined database and container name
<input
aria-invalid="false"
class="ms-TextField-field field-115"
data-test="job-name-textfield"
id="TextField48"
type="text"
value=""
@@ -2617,7 +2585,6 @@ exports[`PreviewCopyJob should render with undefined database and container name
</span>
<span
class="css-125"
data-test="source-subscription-name"
>
Test Subscription
</span>
@@ -2632,7 +2599,6 @@ exports[`PreviewCopyJob should render with undefined database and container name
</span>
<span
class="css-125"
data-test="source-account-name"
>
test-account
</span>

View File

@@ -9,7 +9,7 @@ import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../../Context/CopyJobContext";
import { CopyJobMigrationType } from "../../../../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { AccountDropdown, normalizeAccountId } from "./AccountDropdown";
import { AccountDropdown } from "./AccountDropdown";
jest.mock("../../../../../../hooks/useDatabaseAccounts");
jest.mock("../../../../../../UserContext", () => ({
@@ -202,16 +202,13 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toEqual({
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
});
expect(newState.source.account).toBe(mockDatabaseAccount1);
});
it("should auto-select predefined account from userContext if available", async () => {
const userContextAccount = {
...mockDatabaseAccount2,
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account2",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account2",
};
(userContext as any).databaseAccount = userContextAccount;
@@ -226,10 +223,7 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(mockCopyJobState);
expect(newState.source.account).toEqual({
...mockDatabaseAccount2,
id: normalizeAccountId(mockDatabaseAccount2.id),
});
expect(newState.source.account).toBe(mockDatabaseAccount2);
});
it("should keep current account if it exists in the filtered list", async () => {
@@ -254,16 +248,7 @@ describe("AccountDropdown", () => {
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
expect(newState).toEqual({
...contextWithSelectedAccount.copyJobState,
source: {
...contextWithSelectedAccount.copyJobState.source,
account: {
...mockDatabaseAccount1,
id: normalizeAccountId(mockDatabaseAccount1.id),
},
},
});
expect(newState).toBe(contextWithSelectedAccount.copyJobState);
});
it("should handle account change when user selects different account", async () => {
@@ -287,7 +272,7 @@ describe("AccountDropdown", () => {
it("should normalize account ID for Portal platform", () => {
const portalAccount = {
...mockDatabaseAccount1,
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/account1",
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDb/databaseAccounts/account1",
};
(configContext as any).platform = Platform.Portal;

View File

@@ -12,7 +12,7 @@ import FieldRow from "../../Components/FieldRow";
interface AccountDropdownProps {}
export const normalizeAccountId = (id: string = "") => {
const normalizeAccountId = (id: string) => {
if (configContext.platform === Platform.Portal) {
return id.replace("/Microsoft.DocumentDb/", "/Microsoft.DocumentDB/");
} else if (configContext.platform === Platform.Hosted) {
@@ -27,12 +27,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
const sqlApiOnlyAccounts = (allAccounts || [])
.filter((account) => apiType(account) === "SQL")
.map((account) => ({
...account,
id: normalizeAccountId(account.id),
}));
const sqlApiOnlyAccounts: DatabaseAccount[] = (allAccounts || []).filter((account) => apiType(account) === "SQL");
const updateCopyJobState = (newAccount: DatabaseAccount) => {
setCopyJobState((prevState) => {
@@ -52,8 +47,9 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
useEffect(() => {
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
const currentAccountId = copyJobState?.source?.account?.id;
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
const predefinedAccountId = userContext.databaseAccount?.id;
const selectedAccountId = currentAccountId || predefinedAccountId;
const targetAccount: DatabaseAccount | null =
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
@@ -62,7 +58,7 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
const accountOptions =
sqlApiOnlyAccounts?.map((account) => ({
key: account.id,
key: normalizeAccountId(account.id),
text: account.name,
data: account,
})) || [];

View File

@@ -10,7 +10,7 @@ interface MigrationTypeCheckboxProps {
}
export const MigrationTypeCheckbox: React.FC<MigrationTypeCheckboxProps> = React.memo(({ checked, onChange }) => (
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow" data-test="migration-type-checkbox">
<Stack horizontal horizontalAlign="space-between" className="migrationTypeRow">
<Checkbox label={ContainerCopyMessages.migrationTypeCheckboxLabel} checked={checked} onChange={onChange} />
</Stack>
));

View File

@@ -3,7 +3,6 @@
exports[`MigrationTypeCheckbox Component Rendering should render in checked state 1`] = `
<div
class="ms-Stack migrationTypeRow css-109"
data-test="migration-type-checkbox"
>
<div
class="ms-Checkbox is-checked is-enabled root-119"
@@ -44,7 +43,6 @@ exports[`MigrationTypeCheckbox Component Rendering should render in checked stat
exports[`MigrationTypeCheckbox Component Rendering should render with default props (unchecked state) 1`] = `
<div
class="ms-Stack migrationTypeRow css-109"
data-test="migration-type-checkbox"
>
<div
class="ms-Checkbox is-enabled root-110"

View File

@@ -47,11 +47,7 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
const onDropdownChange = dropDownChangeHandler(setCopyJobState);
return (
<Stack
data-test="Panel:SelectSourceAndTargetContainers"
className="selectSourceAndTargetContainers"
tokens={{ childrenGap: 25 }}
>
<Stack className="selectSourceAndTargetContainers" tokens={{ childrenGap: 25 }}>
<span>{ContainerCopyMessages.selectSourceAndTargetContainersDescription}</span>
<DatabaseContainerSection
heading={ContainerCopyMessages.sourceContainerSubHeading}
@@ -63,7 +59,6 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
selectedContainer={source?.containerId}
containerDisabled={!source?.databaseId}
containerOnChange={onDropdownChange("sourceContainer")}
sectionType="source"
/>
<DatabaseContainerSection
heading={ContainerCopyMessages.targetContainerSubHeading}
@@ -76,7 +71,6 @@ const SelectSourceAndTargetContainers = ({ showAddCollectionPanel }: SelectSourc
containerDisabled={!target?.databaseId}
containerOnChange={onDropdownChange("targetContainer")}
handleOnDemandCreateContainer={showAddCollectionPanel}
sectionType="target"
/>
</Stack>
);

View File

@@ -32,7 +32,6 @@ describe("DatabaseContainerSection", () => {
selectedContainer: "container1",
containerDisabled: false,
containerOnChange: mockContainerOnChange,
sectionType: "source",
};
beforeEach(() => {
@@ -293,7 +292,6 @@ describe("DatabaseContainerSection", () => {
containerOptions: mockContainerOptions,
selectedContainer: "container1",
containerOnChange: mockContainerOnChange,
sectionType: "source",
};
render(<DatabaseContainerSection {...minimalProps} />);
@@ -395,7 +393,6 @@ describe("DatabaseContainerSection", () => {
containerOptions: [{ key: "c1", text: "Container 1", data: { id: "c1" } }],
selectedContainer: "c1",
containerOnChange: jest.fn(),
sectionType: "source",
};
const { container } = render(<DatabaseContainerSection {...minimalProps} />);
@@ -414,7 +411,6 @@ describe("DatabaseContainerSection", () => {
containerDisabled: false,
containerOnChange: jest.fn(),
handleOnDemandCreateContainer: jest.fn(),
sectionType: "target",
};
const { container } = render(<DatabaseContainerSection {...fullProps} />);
@@ -432,7 +428,6 @@ describe("DatabaseContainerSection", () => {
selectedContainer: "container1",
containerDisabled: true,
containerOnChange: jest.fn(),
sectionType: "target",
};
const { container } = render(<DatabaseContainerSection {...disabledProps} />);
@@ -448,7 +443,6 @@ describe("DatabaseContainerSection", () => {
containerOptions: [],
selectedContainer: "",
containerOnChange: jest.fn(),
sectionType: "target",
};
const { container } = render(<DatabaseContainerSection {...emptyOptionsProps} />);

View File

@@ -15,7 +15,6 @@ export const DatabaseContainerSection = ({
containerDisabled,
containerOnChange,
handleOnDemandCreateContainer,
sectionType,
}: DatabaseContainerSectionProps) => (
<Stack tokens={{ childrenGap: 15 }} className="databaseContainerSection">
<label className="subHeading">{heading}</label>
@@ -28,7 +27,6 @@ export const DatabaseContainerSection = ({
disabled={!!databaseDisabled}
selectedKey={selectedDatabase}
onChange={databaseOnChange}
data-test={`${sectionType}-databaseDropdown`}
/>
</FieldRow>
<FieldRow label={ContainerCopyMessages.containerDropdownLabel}>
@@ -41,7 +39,6 @@ export const DatabaseContainerSection = ({
disabled={!!containerDisabled}
selectedKey={selectedContainer}
onChange={containerOnChange}
data-test={`${sectionType}-containerDropdown`}
/>
{handleOnDemandCreateContainer && (
<ActionButton className="create-container-link-btn" onClick={() => handleOnDemandCreateContainer()}>

View File

@@ -37,7 +37,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown98"
role="combobox"
tabindex="0"
@@ -95,7 +94,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with all pro
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown99"
role="combobox"
tabindex="0"
@@ -184,7 +182,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
class="ms-Dropdown is-disabled is-required dropdown-143"
data-is-focusable="false"
data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown103"
role="combobox"
tabindex="-1"
@@ -242,7 +239,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with disable
class="ms-Dropdown is-disabled is-required dropdown-143"
data-is-focusable="false"
data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown104"
role="combobox"
tabindex="-1"
@@ -310,7 +306,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="target-databaseDropdown"
id="Dropdown105"
role="combobox"
tabindex="0"
@@ -368,7 +363,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with empty o
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="target-containerDropdown"
id="Dropdown106"
role="combobox"
tabindex="0"
@@ -436,7 +430,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="source-databaseDropdown"
id="Dropdown96"
role="combobox"
tabindex="0"
@@ -494,7 +487,6 @@ exports[`DatabaseContainerSection Snapshot Testing matches snapshot with minimal
class="ms-Dropdown is-required dropdown-112"
data-is-focusable="true"
data-ktp-target="true"
data-test="source-containerDropdown"
id="Dropdown97"
role="combobox"
tabindex="0"

View File

@@ -83,7 +83,6 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
return (
<IconButton
data-test={`CopyJobActionMenu/Button:${job.Name}`}
role="button"
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
menuProps={{ items: getMenuItems() }}

View File

@@ -80,7 +80,6 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<ShimmeredDetailsList
className="CopyJobListContainer"
onRenderRow={_onRenderRow}
checkboxVisibility={2}
columns={columns}

View File

@@ -49,7 +49,6 @@ export interface DatabaseContainerSectionProps {
containerDisabled?: boolean;
containerOnChange: (ev: React.FormEvent<HTMLDivElement>, option: DropdownOptionType) => void;
handleOnDemandCreateContainer?: () => void;
sectionType: "source" | "target";
}
export interface CopyJobContextState {

View File

@@ -155,7 +155,12 @@ export class ComputedPropertiesComponent extends React.Component<
</Link>
&#160; about how to define computed properties and how to use them.
</Text>
<div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
<div
className="settingsV2Editor"
tabIndex={0}
ref={this.computedPropertiesDiv}
data-test="computed-properties-editor"
></div>
</Stack>
);
}

View File

@@ -187,7 +187,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
<Text styles={textSubHeadingStyle}>Partitioning</Text>
</Stack>
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
<Text styles={textSubHeadingStyle1}>
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
@@ -199,7 +199,6 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
{!isReadOnly && (
<>
<MessageBar
data-test="partition-key-warning"
messageBarType={MessageBarType.warning}
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={darkThemeMessageBarStyles}
@@ -221,7 +220,6 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
</Text>
{configContext.platform !== Platform.Emulator && (
<PrimaryButton
data-test="change-partition-key-button"
styles={{ root: { width: "fit-content" } }}
text="Change"
onClick={startPartitionkeyChangeWorkflow}

View File

@@ -302,8 +302,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: GeospatialConfigType.Geography, text: "Geography" },
{ key: GeospatialConfigType.Geometry, text: "Geometry" },
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
];
private getGeoSpatialComponent = (): JSX.Element => (

View File

@@ -31,6 +31,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
</Text>
<div
className="settingsV2Editor"
data-test="computed-properties-editor"
tabIndex={0}
/>
</Stack>

View File

@@ -78,7 +78,6 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Text>
</Stack>
<Stack
data-test="partition-key-values"
tokens={
{
"childrenGap": 5,
@@ -109,7 +108,6 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Stack>
</Stack>
<StyledMessageBar
data-test="partition-key-warning"
messageBarIconProps={
{
"className": "messageBarWarningIcon",
@@ -162,7 +160,6 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container.
</Text>
<CustomizedPrimaryButton
data-test="change-partition-key-button"
onClick={[Function]}
styles={
{
@@ -240,7 +237,6 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
</Text>
</Stack>
<Stack
data-test="partition-key-values"
tokens={
{
"childrenGap": 5,

View File

@@ -167,10 +167,12 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},
@@ -652,10 +654,12 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},
@@ -1224,10 +1228,12 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},
@@ -1760,10 +1766,12 @@ exports[`SubSettingsComponent renders 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},
@@ -2330,10 +2338,12 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
options={
[
{
"ariaLabel": "geography-option",
"key": "Geography",
"text": "Geography",
},
{
"ariaLabel": "geometry-option",
"key": "Geometry",
"text": "Geometry",
},

View File

@@ -208,7 +208,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</div>
</Stack>
{createNewContainer ? (
<Stack data-test="create-new-container-form">
<Stack>
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
<Stack className="panelGroupSpacing">
<Stack horizontal>
@@ -230,7 +230,6 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</TooltipHost>
</Stack>
<input
data-test="new-container-id-input"
name="collectionId"
id="collectionId"
type="text"
@@ -272,7 +271,6 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
<input
type="text"
data-test="new-container-partition-key-input"
id="addCollection-partitionKeyValue"
aria-required
required
@@ -306,7 +304,6 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
type="text"
id="addCollection-partitionKeyValue"
key={`addCollection-partitionKeyValue_${index}`}
data-test={`new-container-sub-partition-key-input-${index}`}
aria-required
required
size={40}
@@ -330,8 +327,6 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}}
/>
<IconButton
data-test={`remove-sub-partition-key-button-${index}`}
ariaLabel="Remove hierarchical partition key"
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
@@ -344,7 +339,6 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
})}
<Stack className="panelGroupSpacing">
<DefaultButton
data-test="add-sub-partition-key-button"
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
@@ -352,11 +346,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
Add hierarchical partition key
</DefaultButton>
{subPartitionKeys.length > 0 && (
<Text
data-test="hierarchical-partitioning-info-text"
variant="small"
style={{ color: "var(--colorNeutralForeground1)" }}
>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
@@ -369,7 +359,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</Stack>
</Stack>
) : (
<Stack data-test="use-existing-container-form">
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
@@ -400,7 +390,6 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}}
defaultSelectedKey={targetCollectionId}
responsiveMode={999}
ariaLabel="Existing Containers"
/>
</Stack>
)}

View File

@@ -205,7 +205,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
/>
{uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer">
<div className="fileUploadSummaryContainer" data-test="file-upload-status">
<b style={{ color: "var(--colorNeutralForeground1)" }}>File upload status</b>
<DetailsList
items={uploadFileData}

View File

@@ -125,10 +125,7 @@ const App = (): JSX.Element => {
<KeyboardShortcutRoot>
<div className="flexContainer" aria-hidden="false">
{userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? (
<>
<ContainerCopyPanel explorer={explorer} />
<SidePanel />
</>
<ContainerCopyPanel explorer={explorer} />
) : (
<DivExplorer explorer={explorer} />
)}

View File

@@ -164,9 +164,6 @@ $ENV:NOSQL_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<accou
# NoSQL API (Readonly)
$ENV:NOSQL_READONLY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
# NoSQL API (Container Copy)
$ENV:NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken
# Tables API
$ENV:TABLE_TESTACCOUNT_TOKEN=az account get-access-token --scope "https://<account name>.documents.azure.com/.default" -o tsv --query accessToken

View File

@@ -11,7 +11,7 @@ export interface TestNameOptions {
prefixed?: boolean;
}
export function generateUniqueName(baseName: string, options?: TestNameOptions): string {
export function generateUniqueName(baseName, options?: TestNameOptions): string {
const length = options?.length ?? 1;
const timestamp = options?.timestampped === undefined ? true : options.timestampped;
const prefixed = options?.prefixed === undefined ? true : options.prefixed;
@@ -40,7 +40,6 @@ export enum TestAccount {
Mongo32 = "Mongo32",
SQL = "SQL",
SQLReadOnly = "SQLReadOnly",
SQLContainerCopyOnly = "SQLContainerCopyOnly",
}
export const defaultAccounts: Record<TestAccount, string> = {
@@ -52,7 +51,6 @@ export const defaultAccounts: Record<TestAccount, string> = {
[TestAccount.Mongo32]: "github-e2etests-mongo32",
[TestAccount.SQL]: "github-e2etests-sql",
[TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly",
[TestAccount.SQLContainerCopyOnly]: "github-e2etests-sql-containercopyonly",
};
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
@@ -79,14 +77,7 @@ export function getAccountName(accountType: TestAccount) {
);
}
type TestExplorerUrlOptions = {
iframeSrc?: string;
enablecontainercopy?: boolean;
};
export async function getTestExplorerUrl(accountType: TestAccount, options?: TestExplorerUrlOptions): Promise<string> {
const { iframeSrc, enablecontainercopy } = options ?? {};
export async function getTestExplorerUrl(accountType: TestAccount, iframeSrc?: string): Promise<string> {
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken();
const accountName = getAccountName(accountType);
@@ -102,7 +93,6 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes
const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN;
const tableRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN;
const gremlinRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN;
const cassandraRbacToken = process.env.CASSANDRA_TESTACCOUNT_TOKEN;
@@ -118,16 +108,6 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes
}
break;
case TestAccount.SQLContainerCopyOnly:
if (nosqlContainerCopyRbacToken) {
params.set("nosqlRbacToken", nosqlContainerCopyRbacToken);
params.set("enableaaddataplane", "true");
}
if (enablecontainercopy) {
params.set("enablecontainercopy", "true");
}
break;
case TestAccount.SQLReadOnly:
if (nosqlReadOnlyRbacToken) {
params.set("nosqlReadOnlyRbacToken", nosqlReadOnlyRbacToken);
@@ -185,39 +165,6 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes
return `https://localhost:1234/testExplorer.html?${params.toString()}`;
}
type DropdownItemExpectations = {
ariaLabel?: string;
itemCount?: number;
};
type DropdownItemMatcher = {
name?: string;
position?: number;
};
export async function getDropdownItemByNameOrPosition(
frame: Frame,
matcher?: DropdownItemMatcher,
expectedOptions?: DropdownItemExpectations,
): Promise<Locator> {
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
if (expectedOptions?.ariaLabel) {
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(expectedOptions.ariaLabel);
}
if (expectedOptions?.itemCount) {
const items = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
await expect(items).toHaveCount(expectedOptions.itemCount);
}
const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
if (matcher?.name) {
return containerDropdownItems.filter({ hasText: matcher.name });
} else if (matcher?.position !== undefined) {
return containerDropdownItems.nth(matcher.position);
}
// Return first item if no matcher is provided
return containerDropdownItems.first();
}
/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */
class TreeNode {
constructor(
@@ -379,6 +326,7 @@ type PanelOpenOptions = {
export enum CommandBarButton {
Save = "Save",
ExecuteQuery = "Execute Query",
UploadItem = "Upload Item",
}
/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */
@@ -524,7 +472,7 @@ export class DataExplorer {
}
/** Waits for the Data Explorer app to load */
static async waitForExplorer(page: Page, options?: TestExplorerUrlOptions): Promise<DataExplorer> {
static async waitForExplorer(page: Page) {
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();
if (iframeElement === null) {
throw new Error("Explorer iframe not found");
@@ -536,110 +484,15 @@ export class DataExplorer {
throw new Error("Explorer frame not found");
}
if (!options?.enablecontainercopy) {
await explorerFrame?.getByTestId("DataExplorerRoot").waitFor();
}
await explorerFrame?.getByTestId("DataExplorerRoot").waitFor();
return new DataExplorer(explorerFrame);
}
/** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<DataExplorer> {
const url = await getTestExplorerUrl(testAccount, { iframeSrc });
const url = await getTestExplorerUrl(testAccount, iframeSrc);
await page.goto(url);
return DataExplorer.waitForExplorer(page);
}
}
export async function waitForApiResponse(
page: Page,
urlPattern: string,
method?: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payloadValidator?: (payload: any) => boolean,
) {
return page.waitForResponse(async (response) => {
const request = response.request();
if (!request.url().includes(urlPattern)) {
return false;
}
if (method && request.method() !== method) {
return false;
}
if (payloadValidator && (request.method() === "POST" || request.method() === "PUT")) {
const postData = request.postData();
if (postData) {
try {
const payload = JSON.parse(postData);
return payloadValidator(payload);
} catch {
return false;
}
}
}
return true;
});
}
export async function interceptAndInspectApiRequest(
page: Page,
urlPattern: string,
method: string = "POST",
error: Error,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorValidator: (url?: string, payload?: any) => boolean,
): Promise<void> {
await page.route(
(url) => url.pathname.includes(urlPattern),
async (route, request) => {
if (request.method() !== method) {
await route.continue();
return;
}
const postData = request.postData();
if (postData) {
try {
const payload = JSON.parse(postData);
if (errorValidator && errorValidator(request.url(), payload)) {
await route.fulfill({
status: 409,
contentType: "application/json",
body: JSON.stringify({
code: "Conflict",
message: error.message,
}),
});
return;
}
} catch (err) {
if (err instanceof Error && err.message.includes("not allowed")) {
throw err;
}
}
}
await route.continue();
},
);
}
export class ContainerCopy {
constructor(
public frame: Frame,
public wrapper: Locator,
) {}
static async waitForContainerCopy(page: Page): Promise<ContainerCopy> {
const explorerFrame = await DataExplorer.waitForExplorer(page, { enablecontainercopy: true });
const containerCopyWrapper = explorerFrame.frame.locator("div#containerCopyWrapper");
return new ContainerCopy(explorerFrame.frame, containerCopyWrapper);
}
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<ContainerCopy> {
const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true });
await page.goto(url);
return ContainerCopy.waitForContainerCopy(page);
}
}

View File

@@ -1,677 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect, Frame, Locator, Page, test } from "@playwright/test";
import { set } from "lodash";
import { DatabaseAccount, Subscription } from "../../src/Contracts/DataModels";
import { truncateName } from "../../src/Explorer/ContainerCopy/CopyJobUtils";
import {
ContainerCopy,
getAccountName,
getDropdownItemByNameOrPosition,
interceptAndInspectApiRequest,
TestAccount,
waitForApiResponse,
} from "../fx";
test.describe.configure({ mode: "serial" });
let page: Page;
let wrapper: Locator = null!;
let panel: Locator = null!;
let frame: Frame = null!;
let expectedCopyJobNameInitial: string = null!;
let expectedSourceSubscription: any = null!;
let expectedSourceAccount: DatabaseAccount = null!;
let expectedJobName: string = "";
let targetAccountName: string = "";
let expectedSourceAccountName: string = "";
test.beforeAll("Container Copy - Before All", async ({ browser }) => {
page = await browser.newPage();
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
expectedJobName = `test_job_${Date.now()}`;
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
});
test.afterEach("Container Copy - After Each", async () => {
await page.unroute(/.*/, (route) => route.continue());
});
test("Loading and verifying the content of the page", async () => {
expect(wrapper).not.toBeNull();
await expect(wrapper.getByTestId("CommandBar/Button:Create Copy Job")).toBeVisible();
await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible();
await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible();
});
test("Opening the Create Copy Job panel", async () => {
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
await createCopyJobButton.click();
panel = frame.getByTestId("Panel:Create copy job");
await expect(panel).toBeVisible();
await expect(panel.getByRole("heading", { name: "Create copy job" })).toBeVisible();
});
test("select different account dropdown", async () => {
const accountDropdown = panel.getByTestId("account-dropdown");
await accountDropdown.click();
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual("Account");
const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all();
const filteredItems = [];
for (const item of allDropdownItems) {
const testContent = (await item.textContent()) ?? "";
if (testContent.trim() !== targetAccountName.trim()) {
filteredItems.push(item);
}
}
if (filteredItems.length > 0) {
const firstDropdownItem = filteredItems[0];
expectedSourceAccountName = (await firstDropdownItem.textContent()) ?? "";
await firstDropdownItem.click();
} else {
throw new Error("No dropdown items available after filtering");
}
const fluentUiCheckboxContainer = panel.getByTestId("migration-type-checkbox").locator("div.ms-Checkbox");
await fluentUiCheckboxContainer.click();
await panel.getByRole("button", { name: "Next" }).click();
});
test("Verifying Assign Permissions panel for online copy", async () => {
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
await expect(permissionScreen).toBeVisible();
await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible();
await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible();
});
test("Verify Point-in-Time Restore timer and refresh button workflow", async () => {
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}**`, async (route) => {
const mockData = {
identity: {
type: "SystemAssigned",
principalId: "00-11-22-33",
},
properties: {
defaultIdentity: "SystemAssignedIdentity",
backupPolicy: {
type: "Continuous",
},
capabilities: [{ name: "EnableOnlineContainerCopy" }],
},
};
if (route.request().method() === "GET") {
const response = await route.fetch();
const actualData = await response.json();
const mergedData = { ...actualData };
set(mergedData, "identity", mockData.identity);
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mergedData),
});
} else {
await route.continue();
}
});
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
await expect(permissionScreen).toBeVisible();
const expandedAccordionHeader = permissionScreen
.getByTestId("permission-group-container-onlineConfigs")
.locator("button[aria-expanded='true']");
await expect(expandedAccordionHeader).toBeVisible();
const accordionItem = expandedAccordionHeader
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
.first();
const accordionPanel = accordionItem.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']").first();
await page.clock.install({ time: new Date("2024-01-01T10:00:00Z") });
const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn");
await expect(pitrBtn).toBeVisible();
await pitrBtn.click();
page.context().on("page", async (newPage) => {
const expectedUrlEndPattern = new RegExp(
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${expectedSourceAccountName}/backupRestore`,
);
expect(newPage.url()).toMatch(expectedUrlEndPattern);
await newPage.close();
});
const loadingOverlay = frame.locator("[data-test='loading-overlay']");
await expect(loadingOverlay).toBeVisible();
const refreshBtn = accordionPanel.getByTestId("pointInTimeRestore:RefreshBtn");
await expect(refreshBtn).not.toBeVisible();
// Fast forward time by 11 minutes (11 * 60 * 1000ms = 660000ms)
await page.clock.fastForward(11 * 60 * 1000);
await expect(refreshBtn).toBeVisible();
await expect(pitrBtn).not.toBeVisible();
await refreshBtn.click();
await expect(loadingOverlay).toBeVisible();
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
});
test("Veify Popover & Loading Overlay on permission screen with API mocks and accordion interactions", async () => {
await page.route(
`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/sqlRoleAssignments*`,
async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
value: [
{
principalId: "00-11-22-33",
roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/77-88-99`,
},
],
}),
});
},
);
await page.route("**/Microsoft.DocumentDB/databaseAccounts/*/77-88-99**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
value: [
{
name: "00000000-0000-0000-0000-000000000001",
},
],
}),
});
});
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}**`, async (route) => {
const mockData = {
identity: {
type: "SystemAssigned",
principalId: "00-11-22-33",
},
properties: {
defaultIdentity: "SystemAssignedIdentity",
backupPolicy: {
type: "Continuous",
},
capabilities: [{ name: "EnableOnlineContainerCopy" }],
},
};
if (route.request().method() === "PATCH") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ status: "Succeeded" }),
});
} else if (route.request().method() === "GET") {
// Get the actual response and merge with mock data
const response = await route.fetch();
const actualData = await response.json();
const mergedData = { ...actualData };
set(mergedData, "identity", mockData.identity);
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mergedData),
});
} else {
await route.continue();
}
});
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
await expect(permissionScreen).toBeVisible();
const expandedAccordionHeader = permissionScreen
.getByTestId("permission-group-container-crossAccountConfigs")
.locator("button[aria-expanded='true']");
await expect(expandedAccordionHeader).toBeVisible();
const accordionItem = expandedAccordionHeader
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
.first();
const accordionPanel = accordionItem.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']").first();
const toggleButton = accordionPanel.getByTestId("btn-toggle");
await expect(toggleButton).toBeVisible();
await toggleButton.click();
const popover = frame.locator("[data-test='popover-container']");
await expect(popover).toBeVisible();
const yesButton = popover.getByRole("button", { name: /Yes/i });
const noButton = popover.getByRole("button", { name: /No/i });
await expect(yesButton).toBeVisible();
await expect(noButton).toBeVisible();
await yesButton.click();
const loadingOverlay = frame.locator("[data-test='loading-overlay']");
await expect(loadingOverlay).toBeVisible();
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
await expect(popover).toBeHidden({ timeout: 10 * 1000 });
await panel.getByRole("button", { name: "Cancel" }).click();
});
test("Loading and verifying subscription & account dropdown", async () => {
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
await createCopyJobButton.click();
panel = frame.getByTestId("Panel:Create copy job");
await expect(panel).toBeVisible();
const subscriptionPromise = waitForApiResponse(page, "/Microsoft.ResourceGraph/resources", "POST", (payload: any) => {
return (
payload.query.includes("resources | where type == 'microsoft.documentdb/databaseaccounts'") &&
payload.query.includes("| where type == 'microsoft.resources/subscriptions'")
);
});
const accountPromise = waitForApiResponse(page, "/Microsoft.ResourceGraph/resources", "POST", (payload: any) => {
return payload.query.includes("resources | where type =~ 'microsoft.documentdb/databaseaccounts'");
});
const subscriptionResponse = await subscriptionPromise;
const data = await subscriptionResponse.json();
expect(subscriptionResponse.ok()).toBe(true);
const accountResponse = await accountPromise;
const accountData = await accountResponse.json();
expect(accountResponse.ok()).toBe(true);
const selectedSubscription = data.data.find(
(item: Subscription) => item.subscriptionId === process.env.DE_TEST_SUBSCRIPTION_ID,
);
const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
await expect(subscriptionDropdown).toHaveText(new RegExp(selectedSubscription.subscriptionName));
await subscriptionDropdown.click();
const subscriptionItem = await getDropdownItemByNameOrPosition(
frame,
{ name: selectedSubscription.subscriptionName },
{ ariaLabel: "Subscription", itemCount: data.count },
);
await subscriptionItem.click();
const expectedAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
const selectedAccount = accountData.data.find((item: DatabaseAccount) => item.name === expectedAccountName);
const accountDropdown = panel.getByTestId("account-dropdown");
await expect(accountDropdown).toHaveText(new RegExp(expectedAccountName));
await accountDropdown.click();
const accountItem = await getDropdownItemByNameOrPosition(
frame,
{ name: expectedAccountName },
{ ariaLabel: "Account" },
);
await accountItem.click();
expectedSourceSubscription = selectedSubscription;
expectedSourceAccount = selectedAccount;
});
test("Verifying online or offline checkbox", async () => {
/**
* This test verifies the functionality of the migration type checkbox that toggles between
* online and offline container copy modes. It ensures that:
* 1. When online mode is selected, the user is directed to a permissions screen
* 2. When offline mode is selected, the user bypasses the permissions screen
* 3. The UI correctly reflects the selected migration type throughout the workflow
*/
const fluentUiCheckboxContainer = panel.getByTestId("migration-type-checkbox").locator("div.ms-Checkbox");
await fluentUiCheckboxContainer.click();
await panel.getByRole("button", { name: "Next" }).click();
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).toBeVisible();
await expect(panel.getByText("Online container copy", { exact: true })).toBeVisible();
await panel.getByRole("button", { name: "Previous" }).click();
await fluentUiCheckboxContainer.click();
await panel.getByRole("button", { name: "Next" }).click();
await expect(panel.getByTestId("Panel:SelectSourceAndTargetContainers")).toBeVisible();
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).not.toBeVisible();
});
test("Verifying source and target container selection", async () => {
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
expect(sourceContainerDropdown).toBeVisible();
await expect(sourceContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
await sourceDatabaseDropdown.click();
const sourceDbDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 }, { ariaLabel: "Database" });
await sourceDbDropdownItem.click();
await expect(sourceContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
await sourceContainerDropdown.click();
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Container" },
);
await sourceContainerDropdownItem.click();
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
expect(targetContainerDropdown).toBeVisible();
await expect(targetContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
await targetDatabaseDropdown.click();
const targetDbDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 }, { ariaLabel: "Database" });
await targetDbDropdownItem.click();
await expect(targetContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
await targetContainerDropdown.click();
const targetContainerDropdownItem1 = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Container" },
);
await targetContainerDropdownItem1.click();
await panel.getByRole("button", { name: "Next" }).click();
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
await expect(errorContainer).toBeVisible();
await expect(errorContainer).toHaveText(/Source and destination containers cannot be the same/i);
// Reselect target container to be different from source container
await targetContainerDropdown.click();
const targetContainerDropdownItem2 = await getDropdownItemByNameOrPosition(
frame,
{ position: 1 },
{ ariaLabel: "Container" },
);
await targetContainerDropdownItem2.click();
const selectedSourceDatabase = await sourceDatabaseDropdown.innerText();
const selectedSourceContainer = await sourceContainerDropdown.innerText();
const selectedTargetDatabase = await targetDatabaseDropdown.innerText();
const selectedTargetContainer = await targetContainerDropdown.innerText();
expectedCopyJobNameInitial = `${truncateName(selectedSourceDatabase)}.${truncateName(
selectedSourceContainer,
)}_${truncateName(selectedTargetDatabase)}.${truncateName(selectedTargetContainer)}`;
await panel.getByRole("button", { name: "Next" }).click();
await expect(errorContainer).not.toBeVisible();
await expect(panel.getByTestId("Panel:PreviewCopyJob")).toBeVisible();
});
test("Verifying the preview of the copy job", async () => {
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
await expect(previewContainer).toBeVisible();
await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(
expectedSourceSubscription.subscriptionName,
);
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedSourceAccount.name);
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial));
const primaryBtn = panel.getByRole("button", { name: "Copy", exact: true });
await expect(primaryBtn).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
await jobNameInput.fill("test job name");
await expect(primaryBtn).toHaveClass(/(^|\s)is-disabled(\s|$)/);
});
test("Testing API request interception with duplicate job name", async () => {
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
const duplicateJobName = "test-job-name-1";
await jobNameInput.fill(duplicateJobName);
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
const expectedErrorMessage = `Duplicate job name '${duplicateJobName}'`;
await interceptAndInspectApiRequest(
page,
`${expectedSourceAccount.name}/dataTransferJobs/${duplicateJobName}`,
"PUT",
new Error(expectedErrorMessage),
(url?: string) => url?.includes(duplicateJobName) ?? false,
);
let errorThrown = false;
try {
await copyButton.click();
await page.waitForTimeout(2000);
} catch (error: any) {
errorThrown = true;
expect(error.message).toContain("not allowed");
}
if (!errorThrown) {
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
await expect(errorContainer).toBeVisible();
await expect(errorContainer).toHaveText(new RegExp(expectedErrorMessage, "i"));
}
await expect(panel).toBeVisible();
});
test("Testing API request success with valid job name and verifying copy job creation", async () => {
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
const validJobName = expectedJobName;
const copyJobCreationPromise = waitForApiResponse(
page,
`${expectedSourceAccount.name}/dataTransferJobs/${validJobName}`,
"PUT",
);
await jobNameInput.fill(validJobName);
await expect(copyButton).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
await copyButton.click();
const response = await copyJobCreationPromise;
expect(response.ok()).toBe(true);
await expect(panel).not.toBeVisible({ timeout: 10000 });
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
await jobsListContainer.waitFor({ state: "visible" });
const jobItem = jobsListContainer.getByText(validJobName);
await jobItem.waitFor({ state: "visible" });
await expect(jobItem).toBeVisible();
});
test.skip("Pause a running copy job", async () => {
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
await jobsListContainer.waitFor({ state: "visible" });
const firstJobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: expectedJobName });
await firstJobRow.waitFor({ state: "visible" });
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${expectedJobName}`);
await actionMenuButton.waitFor({ state: "visible" });
await actionMenuButton.click();
const pauseAction = frame.locator(".ms-ContextualMenu-list button:has-text('Pause')");
await pauseAction.waitFor({ state: "visible" });
await pauseAction.click();
const updatedJobRow = jobsListContainer.locator(".ms-DetailsRow").filter({ hasText: expectedJobName });
const statusCell = updatedJobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
await expect(statusCell).toContainText(/paused/i, { timeout: 10000 });
});
test.skip("Resume a paused copy job", async () => {
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
await jobsListContainer.waitFor({ state: "visible" });
const pausedJobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: expectedJobName });
await pausedJobRow.waitFor({ state: "visible" });
const statusCell = pausedJobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
await expect(statusCell).toContainText(/paused/i);
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${expectedJobName}`);
await actionMenuButton.waitFor({ state: "visible" });
await actionMenuButton.click();
const resumeAction = frame.locator(".ms-ContextualMenu-list button:has-text('Resume')");
await resumeAction.waitFor({ state: "visible" });
await resumeAction.click();
await expect(statusCell).toContainText(/running|queued/i);
});
test("Cancel a copy job", async () => {
// Create a new job specifically for cancellation testing
const cancelJobName = `cancel_test_job_${Date.now()}`;
// Navigate to create job panel
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
await createCopyJobButton.click();
panel = frame.getByTestId("Panel:Create copy job");
// Skip to container selection (offline mode for faster creation)
await panel.getByRole("button", { name: "Next" }).click();
// Select source containers quickly
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
await sourceDatabaseDropdown.click();
const sourceDatabaseDropdownItem = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Database" },
);
await sourceDatabaseDropdownItem.click();
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
await sourceContainerDropdown.click();
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Container" },
);
await sourceContainerDropdownItem.click();
// Select target containers
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
await targetDatabaseDropdown.click();
const targetDatabaseDropdownItem = await getDropdownItemByNameOrPosition(
frame,
{ position: 0 },
{ ariaLabel: "Database" },
);
await targetDatabaseDropdownItem.click();
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
await targetContainerDropdown.click();
const targetContainerDropdownItem = await getDropdownItemByNameOrPosition(
frame,
{ position: 1 },
{ ariaLabel: "Container" },
);
await targetContainerDropdownItem.click();
await panel.getByRole("button", { name: "Next" }).click();
// Set job name and create
const jobNameInput = panel.getByTestId("job-name-textfield");
await jobNameInput.fill(cancelJobName);
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
// Create job and immediately start polling for it
await copyButton.click();
// Wait for panel to close and job list to refresh
await expect(panel).not.toBeVisible({ timeout: 10000 });
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
await jobsListContainer.waitFor({ state: "visible" });
// Rapid polling to catch the job in running state
let attempts = 0;
const maxAttempts = 50; // Try for ~5 seconds
let jobCancelled = false;
while (attempts < maxAttempts && !jobCancelled) {
try {
// Look for the job row
const jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: cancelJobName });
if (await jobRow.isVisible({ timeout: 100 })) {
const statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
const statusText = await statusCell.textContent({ timeout: 100 });
// If job is still running/queued, try to cancel it
if (statusText && /running|queued|pending/i.test(statusText)) {
const actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${cancelJobName}`);
await actionMenuButton.click({ timeout: 1000 });
const cancelAction = frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')");
if (await cancelAction.isVisible({ timeout: 1000 })) {
await cancelAction.click();
// Verify cancellation
await expect(statusCell).toContainText(/cancelled|canceled|failed/i, { timeout: 5000 });
jobCancelled = true;
break;
}
} else if (statusText && /completed|succeeded|finished/i.test(statusText)) {
// Job completed too fast, skip the test
// console.log(`Job ${cancelJobName} completed too quickly to test cancellation`);
test.skip(true, "Job completed too quickly for cancellation test");
return;
}
}
// Refresh the job list
const refreshButton = wrapper.getByTestId("CommandBar/Button:Refresh");
if (await refreshButton.isVisible({ timeout: 100 })) {
await refreshButton.click();
await page.waitForTimeout(100); // Small delay between attempts
}
} catch (error) {
// Continue trying if there's any error
}
attempts++;
}
if (!jobCancelled) {
// If we couldn't cancel in time, at least verify the job was created
const jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: cancelJobName });
await expect(jobRow).toBeVisible({ timeout: 5000 });
test.skip(true, "Could not catch job in running state for cancellation test");
}
});
test.afterAll("Container Copy - After All", async () => {
await page.unroute(/.*/, (route) => route.continue());
await page.close();
});

View File

@@ -1,7 +1,17 @@
import { expect, test } from "@playwright/test";
import { DataExplorer, DocumentsTab, TestAccount } from "../fx";
import { retry, setPartitionKeys } from "../testData";
import { existsSync, unlinkSync, writeFileSync } from "fs";
import path from "path";
import { CommandBarButton, DataExplorer, DocumentsTab, ONE_MINUTE_MS, TestAccount } from "../fx";
import {
createTestSQLContainer,
itemsPerPartition,
partitionCount,
retry,
setPartitionKeys,
TestContainerContext,
TestData,
} from "../testData";
import { documentTestCases } from "./testCases";
let explorer: DataExplorer = null!;
@@ -95,3 +105,108 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
}
});
}
test.describe.serial("Upload Item", () => {
let context: TestContainerContext = null!;
const uploadDocumentFilePath: string = path.join(__dirname, "uploadDocument.json");
test.beforeEach("Create Test Database and Open documents tab", async ({ page }) => {
context = await createTestSQLContainer();
explorer = await DataExplorer.open(page, TestAccount.SQL);
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
const containerMenuNode = await explorer.waitForContainerItemsNode(context.database.id, context.container.id);
await containerMenuNode.element.click();
});
test.afterEach("Delete Test Database and uploadDocument.json", async () => {
if (existsSync(uploadDocumentFilePath)) {
unlinkSync(uploadDocumentFilePath);
}
await context?.dispose();
});
test("upload document", async () => {
// Create file to upload
const TestDataJsonString: string = JSON.stringify(TestData, null, 2);
writeFileSync(uploadDocumentFilePath, TestDataJsonString);
const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
await uploadItemCommandBar.click();
// Select file to upload
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
await uploadButton.click();
// Verify upload success message
const fileUploadStatusExpected: string = `${partitionCount * itemsPerPartition} created, 0 throttled, 0 errors`;
const fileUploadStatus = explorer.frame.getByTestId("file-upload-status");
await expect(fileUploadStatus).toContainText(fileUploadStatusExpected, {
timeout: ONE_MINUTE_MS,
});
});
test("upload same document twice", async () => {
// Create file to upload
const TestDataJsonString: string = JSON.stringify(TestData, null, 2);
writeFileSync(uploadDocumentFilePath, TestDataJsonString);
const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
await uploadItemCommandBar.click();
// Select file to upload
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
await uploadButton.click();
// Verify upload success message
const fileUploadStatusExpected: string = `${partitionCount * itemsPerPartition} created, 0 throttled, 0 errors`;
const fileUploadStatus = explorer.frame.getByTestId("file-upload-status");
await expect(fileUploadStatus).toContainText(fileUploadStatusExpected, {
timeout: ONE_MINUTE_MS,
});
// Select file to upload again
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
await uploadButton.click();
// Verify upload failure message
const errorIcon = explorer.frame.getByRole("img", { name: "error" });
await expect(errorIcon).toBeVisible({ timeout: ONE_MINUTE_MS });
await expect(fileUploadStatus).toContainText(
`0 created, 0 throttled, ${partitionCount * itemsPerPartition} errors`,
{
timeout: ONE_MINUTE_MS,
},
);
});
test("upload invalid json", async () => {
// Create file to upload
let TestDataJsonString: string = JSON.stringify(TestData, null, 2);
// Remove the first '[' so that it becomes invalid json
TestDataJsonString = TestDataJsonString.substring(1);
writeFileSync(uploadDocumentFilePath, TestDataJsonString);
const uploadItemCommandBar = explorer.commandBarButton(CommandBarButton.UploadItem);
await uploadItemCommandBar.click();
// Select file to upload
await explorer.frame.setInputFiles("#importFileInput", uploadDocumentFilePath);
const uploadButton = explorer.frame.getByTestId("Panel/OkButton");
await uploadButton.click();
// Verify upload failure message
const fileUploadStatusExpected: string = "Unexpected non-whitespace character after JSON";
const fileUploadErrorList = explorer.frame.getByLabel("error list");
await expect(fileUploadErrorList).toContainText(fileUploadStatusExpected, {
timeout: ONE_MINUTE_MS,
});
});
});

View File

@@ -9,7 +9,7 @@ let queryTab: QueryTab = null!;
let queryEditor: Editor = null!;
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer({ includeTestData: true });
context = await createTestSQLContainer(true);
});
test.beforeEach("Open new query tab", async ({ page }) => {

View File

@@ -1,102 +0,0 @@
import { expect, Page, test } from "@playwright/test";
import { DataExplorer, getDropdownItemByNameOrPosition, TestAccount } from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Change Partition Key", () => {
let pageInstance: Page;
let context: TestContainerContext = null!;
let explorer: DataExplorer = null!;
const newPartitionKeyPath = "/newPartitionKey";
const newContainerId = "testcontainer_1";
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer();
});
test.beforeEach("Open container settings", async ({ page }) => {
pageInstance = page;
explorer = await DataExplorer.open(page, TestAccount.SQL);
// Click Scale & Settings and open Partition Key tab
await explorer.openScaleAndSettings(context);
const PartitionKeyTab = explorer.frame.getByTestId("settings-tab-header/PartitionKeyTab");
await PartitionKeyTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
test("Change partition key path", async () => {
await expect(explorer.frame.getByText("/partitionKey")).toBeVisible();
await expect(explorer.frame.getByText("Change partition key")).toBeVisible();
await expect(explorer.frame.getByText(/To safeguard the integrity of/)).toBeVisible();
await expect(explorer.frame.getByText(/To change the partition key/)).toBeVisible();
const changePartitionKeyButton = explorer.frame.getByTestId("change-partition-key-button");
expect(changePartitionKeyButton).toBeVisible();
await changePartitionKeyButton.click();
// Fill out new partition key form in the panel
const changePkPanel = explorer.frame.getByTestId(`Panel:Change partition key`);
await expect(changePkPanel.getByText(context.database.id)).toBeVisible();
await expect(explorer.frame.getByRole("heading", { name: "Change partition key" })).toBeVisible();
await expect(explorer.frame.getByText(/When changing a container/)).toBeVisible();
// Try to switch to new container
await expect(changePkPanel.getByText("New container")).toBeVisible();
await expect(changePkPanel.getByText("Existing container")).toBeVisible();
await expect(changePkPanel.getByTestId("new-container-id-input")).toBeVisible();
changePkPanel.getByTestId("new-container-id-input").fill(newContainerId);
await expect(changePkPanel.getByTestId("new-container-partition-key-input")).toBeVisible();
changePkPanel.getByTestId("new-container-partition-key-input").fill(newPartitionKeyPath);
await expect(changePkPanel.getByTestId("add-sub-partition-key-button")).toBeVisible();
changePkPanel.getByTestId("add-sub-partition-key-button").click();
await expect(changePkPanel.getByTestId("new-container-sub-partition-key-input-0")).toBeVisible();
await expect(changePkPanel.getByTestId("remove-sub-partition-key-button-0")).toBeVisible();
await expect(changePkPanel.getByTestId("hierarchical-partitioning-info-text")).toBeVisible();
changePkPanel.getByTestId("new-container-sub-partition-key-input-0").fill("/customerId");
await changePkPanel.getByTestId("Panel/OkButton").click();
await pageInstance.waitForLoadState("networkidle");
await expect(changePkPanel).not.toBeVisible({ timeout: 60 * 1000 });
// Verify partition key change job
const jobText = explorer.frame.getByText(/Partition key change job/);
await expect(jobText).toBeVisible();
await expect(explorer.frame.locator(".ms-ProgressIndicator-itemName")).toContainText("Portal_testcontainer_1");
const jobRow = explorer.frame.locator(".ms-ProgressIndicator-itemDescription");
await expect(jobRow.getByText("Completed")).toBeVisible({ timeout: 30 * 1000 });
const newContainerNode = await explorer.waitForContainerNode(context.database.id, newContainerId);
expect(newContainerNode).not.toBeNull();
// Now try to switch to existing container
await changePartitionKeyButton.click();
await changePkPanel.getByText("Existing container").click();
await changePkPanel.getByLabel("Use existing container").check();
await changePkPanel.getByText("Choose an existing container").click();
const containerDropdownItem = await getDropdownItemByNameOrPosition(
explorer.frame,
{ name: newContainerId },
{ ariaLabel: "Existing Containers" },
);
await containerDropdownItem.click();
await changePkPanel.getByTestId("Panel/OkButton").click();
await explorer.frame.getByRole("button", { name: "Cancel" }).click();
// Dismiss overlay if it appears
const overlayFrame = explorer.frame.locator("#webpack-dev-server-client-overlay").first();
if (await overlayFrame.count()) {
await overlayFrame.contentFrame().getByLabel("Dismiss").click();
}
const cancelledJobRow = explorer.frame.getByTestId("Tab:tab0");
await expect(cancelledJobRow.getByText("Cancelled")).toBeVisible({ timeout: 30 * 1000 });
});
});

View File

@@ -0,0 +1,103 @@
import { expect, test } from "@playwright/test";
import * as DataModels from "../../../src/Contracts/DataModels";
import { CommandBarButton, DataExplorer, ONE_MINUTE_MS, TestAccount } from "../../fx";
import { createTestSQLContainer, TestContainerContext } from "../../testData";
test.describe("Computed Properties", () => {
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 Settings tab
await explorer.openScaleAndSettings(context);
const computedPropertiesTab = explorer.frame.getByTestId("settings-tab-header/ComputedPropertiesTab");
await computedPropertiesTab.click();
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
test("Add valid computed property", async ({ page }) => {
await clearComputedPropertiesTextBoxContent({ page });
// Create computed property
const computedProperties: DataModels.ComputedProperties = [
{
name: "cp_lowerName",
query: "SELECT VALUE LOWER(c.name) FROM c",
},
];
const computedPropertiesString: string = JSON.stringify(computedProperties);
await page.keyboard.type(computedPropertiesString);
// Save changes
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
await expect(saveButton).toBeEnabled();
await saveButton.click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test("Add computed property with invalid query", async ({ page }) => {
await clearComputedPropertiesTextBoxContent({ page });
// Create computed property with no VALUE keyword in query
const computedProperties: DataModels.ComputedProperties = [
{
name: "cp_lowerName",
query: "SELECT LOWER(c.name) FROM c",
},
];
const computedPropertiesString: string = JSON.stringify(computedProperties);
await page.keyboard.type(computedPropertiesString);
// Save changes
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
await expect(saveButton).toBeEnabled();
await saveButton.click();
await expect(explorer.getConsoleMessage()).toContainText(`Failed to update container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
test("Add computed property with invalid json", async ({ page }) => {
await clearComputedPropertiesTextBoxContent({ page });
// Create computed property with no VALUE keyword in query
const computedProperties: DataModels.ComputedProperties = [
{
name: "cp_lowerName",
query: "SELECT LOWER(c.name) FROM c",
},
];
const computedPropertiesString: string = JSON.stringify(computedProperties);
await page.keyboard.type(computedPropertiesString + "]");
// Save button should remain disabled due to invalid json
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
await expect(saveButton).toBeDisabled();
});
const clearComputedPropertiesTextBoxContent = async ({ page }): Promise<void> => {
// Get computed properties text box
const computedPropertiesTextBox = explorer.frame.getByRole("textbox", { name: "Computed properties" });
await computedPropertiesTextBox.waitFor();
const computedPropertiesEditor = explorer.frame.getByTestId("computed-properties-editor");
await computedPropertiesEditor.click();
// Clear existing content
const isMac: boolean = process.platform === "darwin";
await page.keyboard.press(isMac ? "Meta+A" : "Control+A");
await page.keyboard.press("Backspace");
};
});

View File

@@ -14,7 +14,7 @@ test.describe("Autoscale and Manual throughput", () => {
let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer({ includeTestData: true });
context = await createTestSQLContainer(true);
});
test.beforeEach("Open container settings", async ({ page }) => {

View File

@@ -7,7 +7,7 @@ test.describe("Settings under Scale & Settings", () => {
let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer({ includeTestData: true });
context = await createTestSQLContainer(true);
});
test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => {
@@ -15,7 +15,7 @@ test.describe("Settings under Scale & Settings", () => {
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
// Click Scale & Settings and open Scale tab
// Click Scale & Settings and open Settings tab
await explorer.openScaleAndSettings(context);
const settingsTab = explorer.frame.getByTestId("settings-tab-header/SubSettingsTab");
await settingsTab.click();
@@ -25,46 +25,86 @@ test.describe("Settings under Scale & Settings", () => {
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();
test.describe("Set TTL", () => {
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,
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,
},
);
});
});
test("Update TTL to On (with user entry)", async () => {
const ttlOnRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-option" });
await ttlOnRadioButton.click();
test.describe("Set Geospatial Config", () => {
test("Set Geospatial Config to Geometry then Geography", async () => {
const geometryRadioButton = explorer.frame.getByRole("radio", { name: "geometry-option" });
await geometryRadioButton.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,
},
);
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
});
const geographyRadioButton = explorer.frame.getByRole("radio", { name: "geography-option" });
await geographyRadioButton.click();
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,
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(
`Successfully updated container ${context.container.id}`,
{
timeout: ONE_MINUTE_MS,
},
);
});
});
});

View File

@@ -37,27 +37,35 @@ export interface PartitionKey {
value: string | null;
}
const partitionCount = 4;
export const partitionCount = 4;
// If we increase this number, we need to split bulk creates into multiple batches.
// Bulk operations are limited to 100 items per partition.
const itemsPerPartition = 100;
export const itemsPerPartition = 100;
function createTestItems(): TestItem[] {
const items: TestItem[] = [];
for (let i = 0; i < partitionCount; i++) {
for (let j = 0; j < itemsPerPartition; j++) {
const id = crypto.randomBytes(32).toString("base64");
const id = createSafeRandomString(32);
items.push({
id,
partitionKey: `partition_${i}`,
randomData: crypto.randomBytes(32).toString("base64"),
randomData: createSafeRandomString(32),
});
}
}
return items;
}
// Document IDs cannot contain '/', '\', or '#'
function createSafeRandomString(byteLength: number): string {
return crypto
.randomBytes(byteLength)
.toString("base64")
.replace(/[/\\#]/g, "_");
}
export const TestData: TestItem[] = createTestItems();
export class TestContainerContext {
@@ -74,18 +82,8 @@ export class TestContainerContext {
}
}
type createTestSqlContainerConfig = {
includeTestData?: boolean;
partitionKey?: string;
databaseName?: string;
};
export async function createTestSQLContainer({
includeTestData = false,
partitionKey = "/partitionKey",
databaseName = "",
}: createTestSqlContainerConfig = {}) {
const databaseId = databaseName ? databaseName : generateUniqueName("db");
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
const credentials = getAzureCLICredentials();
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
@@ -114,7 +112,7 @@ export async function createTestSQLContainer({
try {
const { container } = await database.containers.createIfNotExists({
id: containerId,
partitionKey,
partitionKey: "/partitionKey",
});
if (includeTestData) {
const batchCount = TestData.length / 100;

View File

@@ -11,12 +11,8 @@ const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-wes
const selfServeType = urlSearchParams.get("selfServeType") || "example";
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
const authToken = urlSearchParams.get("token");
const enablecontainercopy = urlSearchParams.get("enablecontainercopy");
const nosqlRbacToken =
urlSearchParams.get("nosqlRbacToken") ||
(enablecontainercopy ? process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN : process.env.NOSQL_TESTACCOUNT_TOKEN) ||
"";
const nosqlRbacToken = urlSearchParams.get("nosqlRbacToken") || process.env.NOSQL_TESTACCOUNT_TOKEN || "";
const nosqlReadOnlyRbacToken =
urlSearchParams.get("nosqlReadOnlyRbacToken") || process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN || "";
const tableRbacToken = urlSearchParams.get("tableRbacToken") || process.env.TABLE_TESTACCOUNT_TOKEN || "";
@@ -87,7 +83,6 @@ const initTestExplorer = async (): Promise<void> => {
authorizationToken: `Bearer ${authToken}`,
aadToken: rbacToken,
features: {},
containerCopyEnabled: enablecontainercopy === "true",
hasWriteAccess: true,
csmEndpoint: "https://management.azure.com",
dnsSuffix: "documents.azure.com",