Compare commits

...

6 Commits

Author SHA1 Message Date
asier-isayas
4ac8cd8fe4 Fix playwright tests (#2285)
* Temporarily re-enable key based auth for Mongo and Cassandra tests.

* Increase number of shards for playwright tests.

* Another small bump to test shard count.

* click global new... button then collection in playwright tests

* get new table button

* create and delete container for every individual scale test

* for scale and settings, dont create sample data in container

* run scale tests serially

* refactor scale setup and tear down to be within each test

* record network traces

* record network calls on all retries

* when disposing of database during playwright test, refresh tree to remove deleted database

* refresh tree before  opening scale and settings

* When opening scale and settings, refresh databases

* reload all databases before loading offers

* increase time for change partition key request

* increase time for change partition key request

* refresh databases in test instead of product code

* when refreshing containers, open console window to check for status completion

* close notification console window after seeing desired log

* create and delete a container for each individual test

* dont delete database after every test. leave it to the CI

* Don't refresh databases when opening Scale+Settings and only delete database if running locally

* only open scale and settings at the beginning of each test suite

* get it back to working

* change settings.spect.ts from serial to parallel

* don't delete database after each test

* update container creation throughpout to be 5000

* run tests with no throughput limit on the account

* adjust scale test to reflect no throughput limit on account

* remove test container throughput

* don't refresh collections when clicking settings in product code

* refactor and run cleanup during pr check

* copy cleanup accounts

* run cleanup after playwright tests

* run cleanup every three hours

* revert ci.yml

* update cpk test

* remove cpk

* remove cleanup accounts and add cpk

* add cpk

* remove cpk changes

* revert ci.yml

* run cleanup every two hours

---------

Co-authored-by: Jade Welton <jawelton@microsoft.com>
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2026-01-07 00:36:54 -05:00
BChoudhury-ms
53288dec6f feat: add test identifiers (data-test) to Container Copy components (#2306) 2026-01-06 21:19:58 +05:30
BChoudhury-ms
258a6286e7 refactor(dataTransfers): replace fetch with armRequest for pagination (#2305)
* refactor(dataTransfers): replace fetch with armRequest for pagination

* added comment for next link url parsing
2026-01-06 21:12:19 +05:30
sakshigupta12feb
c8ebca6da4 Fixed homepage UI for fabric (#2304)
* Fixed homePage UI for fabric


Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-01-05 17:02:19 +05:30
BChoudhury-ms
6167f94bc3 fix: restore SidePanel component for Container Copy feature (#2295) 2025-12-29 21:46:47 +05:30
BChoudhury-ms
be89c634f3 Add E2E tests for partition key change workflow (#2293) 2025-12-29 15:08:54 +05:30
52 changed files with 602 additions and 209 deletions

View File

@@ -164,8 +164,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
shardTotal: [16] shardTotal: [20]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Node.js 18.x - name: Use Node.js 18.x
@@ -198,18 +198,18 @@ jobs:
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken) GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN" echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken) # CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN" # echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV # echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken) # MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN" # echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV # echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken) # MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN" # echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV # echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken) # MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN" # echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV # echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3 run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
- name: Upload blob report to GitHub Actions Artifacts - name: Upload blob report to GitHub Actions Artifacts
@@ -250,4 +250,4 @@ jobs:
with: with:
name: html-report--attempt-${{ github.run_attempt }} name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report path: playwright-report
retention-days: 14 retention-days: 14

View File

@@ -6,8 +6,8 @@ on:
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
schedule: schedule:
# Once every hour # Once every two hours
- cron: "0 15 * * *" - cron: "0 */2 * * *"
permissions: permissions:
id-token: write id-token: write
@@ -36,4 +36,4 @@ jobs:
with: with:
node-version: 18.x node-version: 18.x
- run: npm ci - run: npm ci
- run: node utils/cleanupDBs.js - run: node utils/cleanupDBs.js

View File

@@ -218,6 +218,7 @@ a:focus {
.tabPanesContainer { .tabPanesContainer {
overflow: auto !important; overflow: auto !important;
display: flex;
} }
.tabs-container { .tabs-container {

2
package-lock.json generated
View File

@@ -116,8 +116,8 @@
"tinykeys": "2.1.0", "tinykeys": "2.1.0",
"underscore": "1.12.1", "underscore": "1.12.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"web-vitals": "4.2.4",
"uuid": "9.0.0", "uuid": "9.0.0",
"web-vitals": "4.2.4",
"zustand": "3.5.0" "zustand": "3.5.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -11,8 +11,8 @@ export default defineConfig({
reporter: process.env.CI ? "blob" : "html", reporter: process.env.CI ? "blob" : "html",
timeout: 10 * 60 * 1000, timeout: 10 * 60 * 1000,
use: { use: {
trace: "off", trace: "retain-on-failure",
video: "off", video: "retain-on-failure",
screenshot: "on", screenshot: "on",
testIdAttribute: "data-test", testIdAttribute: "data-test",
contextOptions: { contextOptions: {

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import { configContext } from "ConfigContext";
import { ApiType, userContext } from "UserContext"; import { ApiType, userContext } from "UserContext";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { import {
@@ -14,9 +15,12 @@ import {
DataTransferJobFeedResults, DataTransferJobFeedResults,
DataTransferJobGetResults, DataTransferJobGetResults,
} from "Utils/arm/generatedClients/dataTransferService/types"; } from "Utils/arm/generatedClients/dataTransferService/types";
import { armRequest } from "Utils/arm/request";
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs"; import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry"; import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
export const DATA_TRANSFER_JOB_API_VERSION = "2025-05-01-preview";
export interface DataTransferParams { export interface DataTransferParams {
jobName: string; jobName: string;
apiType: ApiType; apiType: ApiType;
@@ -33,26 +37,34 @@ export const getDataTransferJobs = async (
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
accountName: string, accountName: string,
signal?: AbortSignal,
): Promise<DataTransferJobGetResults[]> => { ): Promise<DataTransferJobGetResults[]> => {
let dataTransferJobs: DataTransferJobGetResults[] = []; let dataTransferJobs: DataTransferJobGetResults[] = [];
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount( let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
accountName, accountName,
signal,
); );
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])]; dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
while (dataTransferFeeds?.nextLink) { while (dataTransferFeeds?.nextLink) {
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, { /**
headers: { * The `nextLink` URL returned by the Cosmos DB SQL API pointed to an incorrect endpoint, causing timeouts.
Authorization: userContext.authorizationToken, * (i.e: https://cdbmgmtprodby.documents.azure.com:450/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.DocumentDB/databaseAccounts/{account}/sql/dataTransferJobs?$top=100&$skiptoken=...)
}, * We manipulate the URL by parsing it to extract the path and query parameters,
* then construct the correct URL for the Azure Resource Manager (ARM) API.
* This ensures that the request is made to the correct base URL (`configContext.ARM_ENDPOINT`),
* which is required for ARM operations.
*/
const parsedUrl = new URL(dataTransferFeeds.nextLink);
const nextUrlPath = parsedUrl.pathname + parsedUrl.search;
dataTransferFeeds = await armRequest({
host: configContext.ARM_ENDPOINT,
path: nextUrlPath,
method: "GET",
apiVersion: DATA_TRANSFER_JOB_API_VERSION,
}); });
if (nextResponse.ok) { dataTransferJobs.push(...(dataTransferFeeds?.value || []));
dataTransferFeeds = await nextResponse.json();
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
} else {
break;
}
} }
return dataTransferJobs; return dataTransferJobs;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,6 +78,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Text> </Text>
</Stack> </Stack>
<Stack <Stack
data-test="partition-key-values"
tokens={ tokens={
{ {
"childrenGap": 5, "childrenGap": 5,
@@ -108,6 +109,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
</Stack> </Stack>
</Stack> </Stack>
<StyledMessageBar <StyledMessageBar
data-test="partition-key-warning"
messageBarIconProps={ messageBarIconProps={
{ {
"className": "messageBarWarningIcon", "className": "messageBarWarningIcon",
@@ -160,6 +162,7 @@ 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. 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> </Text>
<CustomizedPrimaryButton <CustomizedPrimaryButton
data-test="change-partition-key-button"
onClick={[Function]} onClick={[Function]}
styles={ styles={
{ {
@@ -237,6 +240,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
</Text> </Text>
</Stack> </Stack>
<Stack <Stack
data-test="partition-key-values"
tokens={ tokens={
{ {
"childrenGap": 5, "childrenGap": 5,

View File

@@ -437,13 +437,14 @@ export default class Explorer {
public onRefreshResourcesClick = async (): Promise<void> => { public onRefreshResourcesClick = async (): Promise<void> => {
if (isFabricMirroredKey()) { if (isFabricMirroredKey()) {
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases()); scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
return; } else {
await (userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases());
await this.refreshNotebookList();
} }
await (userContext.authType === AuthType.ResourceToken logConsoleInfo("Successfully refreshed databases");
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases());
await this.refreshNotebookList();
}; };
// Facade // Facade

View File

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

View File

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

View File

@@ -8,7 +8,8 @@ test("Cassandra keyspace and table CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.Cassandra); const explorer = await DataExplorer.open(page, TestAccount.Cassandra);
await explorer.globalCommandButton("New Table").click(); const newTableButton = await explorer.globalCommandButton("New Table");
await newTableButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"Add Table", "Add Table",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -352,8 +352,9 @@ export class DataExplorer {
* *
* There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button.
*/ */
globalCommandButton(label: string): Locator { async globalCommandButton(label: string): Promise<Locator> {
return this.frame.getByTestId("GlobalCommands").getByText(label); await this.frame.getByTestId("GlobalCommands").click();
return this.frame.getByRole("menuitem", { name: label });
} }
/** Select the command bar button with the specified label */ /** Select the command bar button with the specified label */
@@ -459,6 +460,15 @@ export class DataExplorer {
const containerNode = await this.waitForContainerNode(context.database.id, context.container.id); const containerNode = await this.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand(); await containerNode.expand();
// refresh tree to remove deleted database
const consoleMessages = await this.getNotificationConsoleMessages();
const refreshButton = this.frame.getByTestId("Sidebar/RefreshButton");
await refreshButton.click();
await expect(consoleMessages).toContainText("Successfully refreshed databases", {
timeout: ONE_MINUTE_MS,
});
await this.collapseNotificationConsole();
const scaleAndSettingsButton = this.frame.getByTestId( const scaleAndSettingsButton = this.frame.getByTestId(
`TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`, `TreeNode:${context.database.id}/${context.container.id}/Scale & Settings`,
); );
@@ -466,10 +476,44 @@ export class DataExplorer {
} }
/** Gets the console message element */ /** Gets the console message element */
getConsoleMessage(): Locator { getConsoleHeaderStatus(): Locator {
return this.frame.getByTestId("notification-console/header-status"); return this.frame.getByTestId("notification-console/header-status");
} }
async expandNotificationConsole(): Promise<void> {
await this.setNotificationConsoleExpanded(true);
}
async collapseNotificationConsole(): Promise<void> {
await this.setNotificationConsoleExpanded(false);
}
async setNotificationConsoleExpanded(expanded: boolean): Promise<void> {
const notificationConsoleToggleButton = this.frame.getByTestId("NotificationConsole/ExpandCollapseButton");
const alt = await notificationConsoleToggleButton.locator("img").getAttribute("alt");
// When expanded, the icon says "Collapse icon"
if (expanded && alt === "Expand icon") {
await notificationConsoleToggleButton.click();
} else if (!expanded && alt === "Collapse icon") {
await notificationConsoleToggleButton.click();
}
}
async getNotificationConsoleMessages(): Promise<Locator> {
await this.setNotificationConsoleExpanded(true);
return this.frame.getByTestId("NotificationConsole/Contents");
}
async getDropdownItemByName(name: string, ariaLabel?: string): Promise<Locator> {
const dropdownItemsWrapper = this.frame.locator("div.ms-Dropdown-items");
if (ariaLabel) {
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual(ariaLabel);
}
const containerDropdownItems = dropdownItemsWrapper.locator("button.ms-Dropdown-item[role='option']");
return containerDropdownItems.filter({ hasText: name });
}
/** Waits for the Data Explorer app to load */ /** Waits for the Data Explorer app to load */
static async waitForExplorer(page: Page) { static async waitForExplorer(page: Page) {
const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle(); const iframeElement = await page.getByTestId("DataExplorerFrame").elementHandle();

View File

@@ -9,7 +9,8 @@ test("Gremlin graph CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.Gremlin); const explorer = await DataExplorer.open(page, TestAccount.Gremlin);
// Create new database and graph // Create new database and graph
await explorer.globalCommandButton("New Graph").click(); const newGraphButton = await explorer.globalCommandButton("New Graph");
await newGraphButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Graph", "New Graph",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -14,7 +14,8 @@ import { DataExplorer, TEST_AUTOSCALE_THROUGHPUT_RU, TestAccount, generateUnique
const explorer = await DataExplorer.open(page, accountType); const explorer = await DataExplorer.open(page, accountType);
await explorer.globalCommandButton("New Collection").click(); const newCollectionButton = await explorer.globalCommandButton("New Collection");
await newCollectionButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Collection", "New Collection",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -8,7 +8,8 @@ test("SQL database and container CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.SQL); const explorer = await DataExplorer.open(page, TestAccount.SQL);
await explorer.globalCommandButton("New Container").click(); const newContainerButton = await explorer.globalCommandButton("New Container");
await newContainerButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Container", "New Container",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -9,7 +9,7 @@ let queryTab: QueryTab = null!;
let queryEditor: Editor = null!; let queryEditor: Editor = null!;
test.beforeAll("Create Test Database", async () => { test.beforeAll("Create Test Database", async () => {
context = await createTestSQLContainer(true); context = await createTestSQLContainer({ includeTestData: true });
}); });
test.beforeEach("Open new query tab", async ({ page }) => { test.beforeEach("Open new query tab", async ({ page }) => {
@@ -30,9 +30,12 @@ test.beforeEach("Open new query tab", async ({ page }) => {
await explorer.frame.getByTestId("NotificationConsole/Contents").waitFor(); await explorer.frame.getByTestId("NotificationConsole/Contents").waitFor();
}); });
test.afterAll("Delete Test Database", async () => { // Delete database only if not running in CI
await context?.dispose(); if (!process.env.CI) {
}); test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
}
test("Query results", async () => { test("Query results", async () => {
// Run the query and verify the results // Run the query and verify the results

View File

@@ -0,0 +1,100 @@
// import { expect, test } from "@playwright/test";
// import { DataExplorer, TestAccount } from "../../fx";
// import { createTestSQLContainer, TestContainerContext } from "../../testData";
// test.describe("Change Partition Key", () => {
// 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 }) => {
// 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 expect(PartitionKeyTab).toBeVisible();
// await PartitionKeyTab.click();
// });
// // Delete database only if not running in CI
// if (!process.env.CI) {
// test.afterEach("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();
// await changePkPanel.getByTestId("remove-sub-partition-key-button-0").click();
// await changePkPanel.getByTestId("Panel/OkButton").click();
// await expect(changePkPanel).not.toBeVisible({ timeout: 5 * 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("Pending")).toBeVisible({ timeout: 30 * 1000 });
// await expect(jobRow.getByText("Completed")).toBeVisible({ timeout: 5 * 60 * 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 explorer.getDropdownItemByName(newContainerId, "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

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

View File

@@ -6,14 +6,10 @@ test.describe("Settings under Scale & Settings", () => {
let context: TestContainerContext = null!; let context: TestContainerContext = null!;
let explorer: DataExplorer = null!; let explorer: DataExplorer = null!;
test.beforeAll("Create Test Database", async () => { test.beforeAll("Create Test Database & Open Settings tab", async ({ browser }) => {
context = await createTestSQLContainer(true); context = await createTestSQLContainer();
}); const page = await browser.newPage();
test.beforeEach("Open Settings tab under Scale & Settings", async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQL); explorer = await DataExplorer.open(page, TestAccount.SQL);
const containerNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
await containerNode.expand();
// Click Scale & Settings and open Scale tab // Click Scale & Settings and open Scale tab
await explorer.openScaleAndSettings(context); await explorer.openScaleAndSettings(context);
@@ -21,18 +17,24 @@ test.describe("Settings under Scale & Settings", () => {
await settingsTab.click(); await settingsTab.click();
}); });
test.afterAll("Delete Test Database", async () => { // Delete database only if not running in CI
await context?.dispose(); if (!process.env.CI) {
}); test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
}
test("Update TTL to On (no default)", async () => { test("Update TTL to On (no default)", async () => {
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" }); const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click(); await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click(); await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, { await expect(explorer.getConsoleHeaderStatus()).toContainText(
timeout: ONE_MINUTE_MS, `Successfully updated container ${context.container.id}`,
}); {
timeout: 2 * ONE_MINUTE_MS,
},
);
}); });
test("Update TTL to On (with user entry)", async () => { test("Update TTL to On (with user entry)", async () => {
@@ -44,27 +46,11 @@ test.describe("Settings under Scale & Settings", () => {
await ttlInput.fill("30000"); await ttlInput.fill("30000");
await explorer.commandBarButton(CommandBarButton.Save).click(); await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, { await expect(explorer.getConsoleHeaderStatus()).toContainText(
timeout: ONE_MINUTE_MS, `Successfully updated container ${context.container.id}`,
}); {
}); timeout: 2 * ONE_MINUTE_MS,
},
test("Update TTL to Off", async () => { );
// By default TTL is set to off so we need to first set it to On
const ttlOnNoDefaultRadioButton = explorer.frame.getByRole("radio", { name: "ttl-on-no-default-option" });
await ttlOnNoDefaultRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
// Set it to Off
const ttlOffRadioButton = explorer.frame.getByRole("radio", { name: "ttl-off-option" });
await ttlOffRadioButton.click();
await explorer.commandBarButton(CommandBarButton.Save).click();
await expect(explorer.getConsoleMessage()).toContainText(`Successfully updated container ${context.container.id}`, {
timeout: ONE_MINUTE_MS,
});
}); });
}); });

View File

@@ -7,7 +7,8 @@ test("Tables CRUD", async ({ page }) => {
const explorer = await DataExplorer.open(page, TestAccount.Tables); const explorer = await DataExplorer.open(page, TestAccount.Tables);
await explorer.globalCommandButton("New Table").click(); const newTableButton = explorer.frame.getByTestId("GlobalCommands").getByRole("button", { name: "New Table" });
await newTableButton.click();
await explorer.whilePanelOpen( await explorer.whilePanelOpen(
"New Table", "New Table",
async (panel, okButton) => { async (panel, okButton) => {

View File

@@ -74,8 +74,18 @@ export class TestContainerContext {
} }
} }
export async function createTestSQLContainer(includeTestData?: boolean) { type createTestSqlContainerConfig = {
const databaseId = generateUniqueName("db"); includeTestData?: boolean;
partitionKey?: string;
databaseName?: string;
};
export async function createTestSQLContainer({
includeTestData = false,
partitionKey = "/partitionKey",
databaseName = "",
}: createTestSqlContainerConfig = {}) {
const databaseId = databaseName ? databaseName : generateUniqueName("db");
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique
const credentials = getAzureCLICredentials(); const credentials = getAzureCLICredentials();
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials); const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
@@ -104,7 +114,7 @@ export async function createTestSQLContainer(includeTestData?: boolean) {
try { try {
const { container } = await database.containers.createIfNotExists({ const { container } = await database.containers.createIfNotExists({
id: containerId, id: containerId,
partitionKey: "/partitionKey", partitionKey,
}); });
if (includeTestData) { if (includeTestData) {
const batchCount = TestData.length / 100; const batchCount = TestData.length / 100;

View File

@@ -74,17 +74,50 @@ async function main() {
} }
} else if (account.kind === "GlobalDocumentDB") { } else if (account.kind === "GlobalDocumentDB") {
const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name); const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name);
for (const database of sqlDatabases) { const sqlDatabasesToDelete = sqlDatabases.map(async (database) => {
const timestamp = Number(database.resource._ts) * 1000; await deleteWithRetry(client, database, account.name);
if (timestamp && timestamp < thirtyMinutesAgo) { });
await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name); await Promise.all(sqlDatabasesToDelete);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`); }
} else { }
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`); }
}
// Retry logic for handling throttling
async function deleteWithRetry(client, database, accountName) {
const maxRetries = 5;
let attempt = 0;
let backoffTime = 1000; // Start with 1 second
while (attempt < maxRetries) {
try {
const timestamp = Number(database.resource._ts) * 1000;
if (timestamp && timestamp < thirtyMinutesAgo) {
await client.sqlResources.deleteSqlDatabase(resourceGroupName, accountName, database.name);
console.log(`DELETED: ${accountName} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else {
console.log(`SKIPPED: ${accountName} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
}
return;
} catch (error) {
if (error.statusCode === 429) {
// Throttling error (HTTP 429), apply exponential backoff
console.log(`Throttling detected, retrying ${database.name}... (Attempt ${attempt + 1})`);
await delay(backoffTime);
attempt++;
backoffTime *= 2; // Exponential backoff
} else {
// For other errors, log and break
console.error(`Error deleting ${database.name}:`, error);
break;
} }
} }
} }
console.log(`Failed to delete ${database.name} after ${maxRetries} attempts.`);
}
// Helper function to delay the retry attempts
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
} }
main() main()
@@ -96,4 +129,4 @@ main()
console.log(err); console.log(err);
console.log("Cleanup failed! Exiting with success. Cleanup should always fail safe."); console.log("Cleanup failed! Exiting with success. Cleanup should always fail safe.");
process.exit(0); process.exit(0);
}); });