mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-19 01:35:49 +00:00
Compare commits
1 Commits
master
...
user/bchou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79b55ffd54 |
21
.env.example
21
.env.example
@@ -1 +1,20 @@
|
||||
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
|
||||
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
|
||||
|
||||
# Azure Test Environment Variables
|
||||
# These are automatically set by the global-setup.ts script
|
||||
|
||||
# Azure resource group containing the test resources
|
||||
DE_TEST_RESOURCE_GROUP=bchoudhury-e2e-testing
|
||||
|
||||
# Azure subscription ID for testing
|
||||
DE_TEST_SUBSCRIPTION_ID=074d02eb-4d74-486a-b299-b262264d1536
|
||||
|
||||
# Prefix used for test resource names
|
||||
DE_TEST_ACCOUNT_PREFIX=bchoudhury-e2e-
|
||||
|
||||
# Access token for NoSQL container copy operations
|
||||
# This is generated automatically by the setup script
|
||||
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=
|
||||
|
||||
# Optional: Set to true to enable debug logging during setup
|
||||
DEBUG=false
|
||||
71
.github/agents/playwright-test-generator.agent.md
vendored
Normal file
71
.github/agents/playwright-test-generator.agent.md
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: playwright-test-generator
|
||||
description: 'Expert agent for generating robust Playwright tests for Cosmos DB. Supports DataExplorer and CopyJob flows. Required Inputs: <test-suite>, <test-name>, <test-file>, <seed-file>, <body>'
|
||||
tools:
|
||||
- azure-mcp/search
|
||||
- playwright/browser_click
|
||||
- playwright/browser_drag
|
||||
- playwright/browser_evaluate
|
||||
- playwright/browser_file_upload
|
||||
- playwright/browser_handle_dialog
|
||||
- playwright/browser_hover
|
||||
- playwright/browser_navigate
|
||||
- playwright/browser_press_key
|
||||
- playwright/browser_select_option
|
||||
- playwright/browser_snapshot
|
||||
- playwright/browser_type
|
||||
- playwright/browser_wait_for
|
||||
- filesystem/read_file
|
||||
- filesystem/write_file
|
||||
- filesystem/list_directory
|
||||
- filesystem/create_directory
|
||||
model: Claude Sonnet 4
|
||||
---
|
||||
|
||||
You are an expert Playwright Test Generator from given planner specs.
|
||||
Your specialty is creating tests that match the project's high standards for frame navigation and FluentUI interaction. Your expertise includes functional testing using Playwright
|
||||
|
||||
# Pre-Generation Analysis
|
||||
Before generating code, you MUST:
|
||||
1. **Analyze Existing Specs:** Examine the provided `<seed-file>` and any referenced test files in the project to learn the specific coding standards, naming conventions, and import structures used.
|
||||
2. **Identify the Flow Type:** Determine if the scenario is a **DataExplorer** flow or a **CopyJob** flow.
|
||||
- **DataExplorer Flows:** Use `DataExplorer.open()` in the `beforeEach` or `beforeAll` setup logic.
|
||||
- **CopyJob Flows:** Use `ContainerCopy.open()` in the `beforeEach` or `beforeAll` setup logic.
|
||||
|
||||
# Core Project Standards
|
||||
- **Authentication:** Assume pre-flight authentication (az login/globalSetup) is complete. Do not generate login steps. The `global-setup` has been added into playwright.config.ts
|
||||
- **Library Usage:** Prioritize common helper methods from `fx.ts`.
|
||||
- **Page Hierarchy:**
|
||||
- `frame`: Parent blade locator (used for overlays, dropdown lists, global portal buttons).
|
||||
- `wrapper`: Child locator (used for the specific view content/form inputs).
|
||||
- **Selector Strategy:** Use FluentUI-friendly locators: `getByRole` and `getByLabel`. Also prioritize `getByTestId` where available - check for `testIdAttribute` in playwright.config.ts as "data-test" attributes are configured as "data-testid".
|
||||
|
||||
# Step-by-Step Generation Workflow
|
||||
1. **Plan Parsing:** Extract steps and verification logic from the planner spec.
|
||||
2. **Setup:** Run `generator_setup_page` using the `<seed-file>`.
|
||||
3. **Observation:** Use Playwright tools to execute steps. Pay close attention to which elements require the `frame` context vs. the `wrapper` context.
|
||||
4. **Source Code Generation:**
|
||||
- **File Naming:** Use fs-friendly names based on the scenario.
|
||||
- **Structure:** Encapsulate in a `test.describe` matching the test plan group.
|
||||
- **Code Style:** Match the patterns found in the analyzed existing files exactly.
|
||||
- **Cleanup:** Ensure the test handles data cleanup to remain idempotent.
|
||||
|
||||
# Example Logic (Flow Detection)
|
||||
```ts
|
||||
// If CopyJob flow:
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await ContainerCopy.open // Standardized entry point
|
||||
});
|
||||
(or)
|
||||
test.beforeAll(async ({ page }) => {
|
||||
await ContainerCopy.open // Standardized entry point
|
||||
});
|
||||
|
||||
// If DataExplorer flow:
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await DataExplorer.open // Standardized entry point
|
||||
});
|
||||
(or)
|
||||
test.beforeAll(async ({ page }) => {
|
||||
await DataExplorer.open // Standardized entry point
|
||||
});
|
||||
61
.github/agents/playwright-test-healer.agent.md
vendored
Normal file
61
.github/agents/playwright-test-healer.agent.md
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: playwright-test-healer
|
||||
description: Use this agent when you need to debug and fix failing Playwright tests
|
||||
tools:
|
||||
- search
|
||||
- edit
|
||||
- playwright-test/browser_console_messages
|
||||
- playwright-test/browser_evaluate
|
||||
- playwright-test/browser_generate_locator
|
||||
- playwright-test/browser_network_requests
|
||||
- playwright-test/browser_snapshot
|
||||
- playwright-test/test_debug
|
||||
- playwright-test/test_list
|
||||
- playwright-test/test_run
|
||||
model: Claude Sonnet 4
|
||||
mcp-servers:
|
||||
playwright-test:
|
||||
type: stdio
|
||||
command: npx
|
||||
args:
|
||||
- playwright
|
||||
- run-test-mcp-server
|
||||
tools:
|
||||
- "*"
|
||||
---
|
||||
|
||||
You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix broken Playwright tests using a methodical approach.
|
||||
|
||||
Your workflow:
|
||||
1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests
|
||||
2. **Debug failed tests**: For each failing test run `test_debug`.
|
||||
3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
|
||||
- Examine the error details
|
||||
- Capture page snapshot to understand the context
|
||||
- Analyze selectors, timing issues, or assertion failures
|
||||
4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
|
||||
- Element selectors that may have changed
|
||||
- Timing and synchronization issues
|
||||
- Data dependencies or test environment problems
|
||||
- Application changes that broke test assumptions
|
||||
5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
|
||||
- Updating selectors to match current application state
|
||||
- Fixing assertions and expected values
|
||||
- Improving test reliability and maintainability
|
||||
- For inherently dynamic data, utilize regular expressions to produce resilient locators
|
||||
6. **Verification**: Restart the test after each fix to validate the changes
|
||||
7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly
|
||||
|
||||
Key principles:
|
||||
- Be systematic and thorough in your debugging approach
|
||||
- Document your findings and reasoning for each fix
|
||||
- Prefer robust, maintainable solutions over quick hacks
|
||||
- Use Playwright best practices for reliable test automation
|
||||
- If multiple errors exist, fix them one at a time and retest
|
||||
- Provide clear explanations of what was broken and how you fixed it
|
||||
- You will continue this process until the test runs successfully without any failures or errors.
|
||||
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme()
|
||||
so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead
|
||||
of the expected behavior.
|
||||
- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
|
||||
- Never wait for networkidle or use other discouraged or deprecated apis
|
||||
78
.github/agents/playwright-test-planner.agent.md
vendored
Normal file
78
.github/agents/playwright-test-planner.agent.md
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: playwright-test-planner
|
||||
description: Use this agent when you need to create comprehensive test plan for a web application or website
|
||||
tools:
|
||||
- search
|
||||
- playwright-test/browser_click
|
||||
- playwright-test/browser_close
|
||||
- playwright-test/browser_console_messages
|
||||
- playwright-test/browser_drag
|
||||
- playwright-test/browser_evaluate
|
||||
- playwright-test/browser_file_upload
|
||||
- playwright-test/browser_handle_dialog
|
||||
- playwright-test/browser_hover
|
||||
- playwright-test/browser_navigate
|
||||
- playwright-test/browser_navigate_back
|
||||
- playwright-test/browser_network_requests
|
||||
- playwright-test/browser_press_key
|
||||
- playwright-test/browser_select_option
|
||||
- playwright-test/browser_snapshot
|
||||
- playwright-test/browser_take_screenshot
|
||||
- playwright-test/browser_type
|
||||
- playwright-test/browser_wait_for
|
||||
- playwright-test/planner_setup_page
|
||||
- playwright-test/planner_save_plan
|
||||
model: Claude Sonnet 4
|
||||
mcp-servers:
|
||||
playwright-test:
|
||||
type: stdio
|
||||
command: npx
|
||||
args:
|
||||
- playwright
|
||||
- run-test-mcp-server
|
||||
tools:
|
||||
- "*"
|
||||
---
|
||||
|
||||
You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage planning.
|
||||
|
||||
You will:
|
||||
|
||||
1. **Navigate and Explore**
|
||||
- Invoke the `planner_setup_page` tool once to set up page before using any other tools
|
||||
- Explore the browser snapshot
|
||||
- Do not take screenshots unless absolutely necessary
|
||||
- Use `browser_*` tools to navigate and discover interface
|
||||
- Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality
|
||||
|
||||
2. **Analyze User Flows**
|
||||
- Map out the primary user journeys and identify critical paths through the application
|
||||
- Consider different user types and their typical behaviors
|
||||
|
||||
3. **Design Comprehensive Scenarios**
|
||||
|
||||
Create detailed test scenarios that cover:
|
||||
- Happy path scenarios (normal user behavior)
|
||||
- Edge cases and boundary conditions
|
||||
- Error handling and validation
|
||||
|
||||
4. **Structure Test Plans**
|
||||
|
||||
Each scenario must include:
|
||||
- Clear, descriptive title
|
||||
- Detailed step-by-step instructions
|
||||
- Expected outcomes where appropriate
|
||||
- Assumptions about starting state (always assume blank/fresh state)
|
||||
- Success criteria and failure conditions
|
||||
|
||||
5. **Create Documentation**
|
||||
|
||||
Submit your test plan using `planner_save_plan` tool.
|
||||
|
||||
**Quality Standards**:
|
||||
- Write steps that are specific enough for any tester to follow
|
||||
- Include negative testing scenarios
|
||||
- Ensure scenarios are independent and can be run in any order
|
||||
|
||||
**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and
|
||||
professional formatting suitable for sharing with development and QA teams.
|
||||
34
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
34
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: "Copilot Setup Steps"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
|
||||
jobs:
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
# Customize this step as needed
|
||||
- name: Build application
|
||||
run: npx run build
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,3 +21,4 @@ GettingStarted-ignore*.ipynb
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
|
||||
23
.vscode/mcp.json
vendored
Normal file
23
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"servers": {
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@playwright/mcp@latest"],
|
||||
"env": {
|
||||
"ALLOWED_DIRECTORIES": "C:/Users/bchoudhury/MyProjects/cosmos-explorer"
|
||||
}
|
||||
},
|
||||
"filesystem": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem@latest",
|
||||
"C:/Users/bchoudhury/MyProjects/cosmos-explorer/test",
|
||||
"C:/Users/bchoudhury/MyProjects/cosmos-explorer/src"
|
||||
]
|
||||
}
|
||||
},
|
||||
"inputs": []
|
||||
}
|
||||
27
package-lock.json
generated
27
package-lock.json
generated
@@ -125,7 +125,7 @@
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -10321,12 +10321,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
|
||||
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
|
||||
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.49.1"
|
||||
"playwright": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -30903,12 +30904,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
|
||||
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.1"
|
||||
"playwright-core": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -30921,10 +30923,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.49.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
|
||||
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
|
||||
10
package.json
10
package.json
@@ -111,8 +111,8 @@
|
||||
"tinykeys": "2.1.0",
|
||||
"underscore": "1.12.1",
|
||||
"utility-types": "3.10.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"uuid": "9.0.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"zustand": "3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -120,7 +120,7 @@
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -213,7 +213,11 @@
|
||||
"copyToConsumers": "node copyToConsumers",
|
||||
"test": "rimraf coverage && jest",
|
||||
"test:debug": "jest --runInBand",
|
||||
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:setup:verify": "npx playwright test test/setup-verification.spec.ts",
|
||||
"test:e2e:setup": "node setup-tests.js",
|
||||
"test:e2e:headed": "npx playwright test --headed",
|
||||
"test:e2e:ui": "npx playwright test --ui",
|
||||
"test:file": "jest --coverage=false",
|
||||
"watch": "npm run start",
|
||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||
|
||||
@@ -6,15 +6,17 @@ export default defineConfig({
|
||||
testDir: "test",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
retries: process.env.CI ? 3 : 2,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? "blob" : "html",
|
||||
timeout: 10 * 60 * 1000,
|
||||
globalSetup: require.resolve('./test/global-setup.ts'),
|
||||
use: {
|
||||
trace: "retain-on-failure",
|
||||
video: "retain-on-failure",
|
||||
screenshot: "on",
|
||||
testIdAttribute: "data-test",
|
||||
// storageState: './test/../playwright/.auth/user.json',
|
||||
contextOptions: {
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
|
||||
48
setup-tests copy.js
Normal file
48
setup-tests copy.js
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Manual setup script to run the pre-flight Azure authentication and configuration
|
||||
* This can be used independently of Playwright for setting up the testing environment
|
||||
*/
|
||||
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const path = require('path');
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function runManualSetup() {
|
||||
console.log('🚀 Starting manual Azure Cosmos DB Explorer test setup...');
|
||||
|
||||
try {
|
||||
// Import the global setup function and run it
|
||||
const globalSetup = require('./test/global-setup.js');
|
||||
|
||||
// Create a minimal config object that matches what Playwright would pass
|
||||
const mockConfig = {
|
||||
configFile: path.join(__dirname, 'playwright.config.ts'),
|
||||
rootDir: __dirname,
|
||||
testDir: path.join(__dirname, 'test'),
|
||||
projects: []
|
||||
};
|
||||
|
||||
await globalSetup(mockConfig);
|
||||
|
||||
console.log('✅ Manual setup completed successfully!');
|
||||
console.log('\nYou can now run your Playwright tests with:');
|
||||
console.log(' npm run test:e2e');
|
||||
console.log(' or');
|
||||
console.log(' npx playwright test');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Manual setup failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Only run if this script is executed directly
|
||||
if (require.main === module) {
|
||||
runManualSetup();
|
||||
}
|
||||
|
||||
module.exports = { runManualSetup };
|
||||
48
setup-tests.ts
Normal file
48
setup-tests.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Manual setup script to run the pre-flight Azure authentication and configuration
|
||||
* This can be used independently of Playwright for setting up the testing environment
|
||||
*/
|
||||
|
||||
|
||||
async function runManualSetup(): Promise<void> {
|
||||
console.log('🚀 Starting manual Azure Cosmos DB Explorer test setup...');
|
||||
|
||||
try {
|
||||
// Dynamically import the Playwright global setup
|
||||
const globalSetupModule = await import('./test/global-setup');
|
||||
const globalSetup = globalSetupModule.default;
|
||||
|
||||
if (typeof globalSetup !== 'function') {
|
||||
throw new Error('global-setup.ts does not export a default function');
|
||||
}
|
||||
|
||||
// Minimal mock config similar to what Playwright provides
|
||||
/* const mockConfig: FullConfig = {
|
||||
configFile: path.join(__dirname, 'playwright.config.ts'),
|
||||
rootDir: __dirname,
|
||||
testDir: path.join(__dirname, 'test'),
|
||||
projects: []
|
||||
}; */
|
||||
|
||||
await globalSetup();
|
||||
|
||||
console.log('✅ Manual setup completed successfully!');
|
||||
console.log('\nYou can now run your Playwright tests with:');
|
||||
console.log(' npm run test:e2e');
|
||||
console.log(' or');
|
||||
console.log(' npx playwright test');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Manual setup failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Only run if executed directly (not imported)
|
||||
if (require.main === module) {
|
||||
void runManualSetup();
|
||||
}
|
||||
|
||||
export { runManualSetup };
|
||||
|
||||
@@ -184,10 +184,5 @@ export default {
|
||||
Skipped: "Cancelled",
|
||||
Cancelled: "Cancelled",
|
||||
},
|
||||
dialog: {
|
||||
heading: "",
|
||||
confirmButtonText: "Confirm",
|
||||
cancelButtonText: "Cancel",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
@@ -6,20 +5,6 @@ import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../E
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||
|
||||
const mockShowOkCancelModalDialog = jest.fn();
|
||||
const mockCloseDialog = jest.fn();
|
||||
const mockOpenDialog = jest.fn();
|
||||
|
||||
jest.mock("../../../Controls/Dialog", () => ({
|
||||
useDialog: {
|
||||
getState: () => ({
|
||||
showOkCancelModalDialog: mockShowOkCancelModalDialog,
|
||||
closeDialog: mockCloseDialog,
|
||||
openDialog: mockOpenDialog,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../ContainerCopyMessages", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -33,11 +18,6 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
||||
cancel: "Cancel",
|
||||
complete: "Complete",
|
||||
},
|
||||
dialog: {
|
||||
heading: "Confirm Action",
|
||||
confirmButtonText: "Confirm",
|
||||
cancelButtonText: "Cancel",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -70,9 +50,6 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockShowOkCancelModalDialog.mockClear();
|
||||
mockCloseDialog.mockClear();
|
||||
mockOpenDialog.mockClear();
|
||||
});
|
||||
|
||||
describe("Component Rendering", () => {
|
||||
@@ -289,29 +266,7 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should show confirmation dialog when cancel action is clicked", () => {
|
||||
const job = createMockJob({ Name: "Test Job", Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.any(Object), // dialogBody content
|
||||
);
|
||||
});
|
||||
|
||||
it("should call handleClick when dialog is confirmed for cancel action", () => {
|
||||
it("should call handleClick when cancel action is clicked", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
@@ -322,9 +277,6 @@ describe("CopyJobActionMenu", () => {
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
@@ -342,33 +294,7 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should show confirmation dialog when complete action is clicked", () => {
|
||||
const job = createMockJob({
|
||||
Name: "Test Online Job",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.any(Object), // dialogBody content
|
||||
);
|
||||
});
|
||||
|
||||
it("should call handleClick when dialog is confirmed for complete action", () => {
|
||||
it("should call handleClick when complete action is clicked", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
@@ -382,87 +308,10 @@ describe("CopyJobActionMenu", () => {
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dialog Body Content", () => {
|
||||
it("should pass correct dialog body content for cancel action", () => {
|
||||
const job = createMockJob({ Name: "MyTestJob", Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.objectContaining({
|
||||
props: expect.objectContaining({
|
||||
tokens: expect.any(Object),
|
||||
children: expect.any(Array),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass correct dialog body content for complete action", () => {
|
||||
const job = createMockJob({
|
||||
Name: "OnlineTestJob",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.objectContaining({
|
||||
props: expect.objectContaining({
|
||||
tokens: expect.any(Object),
|
||||
children: expect.any(Array),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show dialog body for actions without confirmation", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disabled States During Updates", () => {
|
||||
const TestComponentWrapper: React.FC<{
|
||||
job: CopyJobType;
|
||||
@@ -490,13 +339,8 @@ describe("CopyJobActionMenu", () => {
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButtonAfterClick = screen.getByText("Pause").closest("button");
|
||||
const pauseButtonAfterClick = screen.getByText("Pause");
|
||||
expect(pauseButtonAfterClick).toBeInTheDocument();
|
||||
expect(pauseButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const cancelButtonAfterClick = screen.getByText("Cancel").closest("button");
|
||||
expect(cancelButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("should not disable actions for different jobs when one is updating", () => {
|
||||
@@ -516,6 +360,22 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should properly handle multiple action types being disabled for the same job", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Pause"));
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Cancel"));
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle complete action disabled state for online jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
@@ -602,7 +462,6 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||
expect(actionButton).toHaveAttribute("role", "button");
|
||||
|
||||
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
||||
expect(moreIcon || actionButton).toBeInTheDocument();
|
||||
@@ -749,129 +608,4 @@ describe("CopyJobActionMenu", () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Complete Coverage Tests", () => {
|
||||
it("should handle all possible dialog scenarios", () => {
|
||||
const dialogTests = [
|
||||
{ action: CopyJobActions.cancel, status: CopyJobStatusType.InProgress, shouldShowDialog: true },
|
||||
{
|
||||
action: CopyJobActions.complete,
|
||||
status: CopyJobStatusType.InProgress,
|
||||
mode: CopyJobMigrationType.Online,
|
||||
shouldShowDialog: true,
|
||||
},
|
||||
{ action: CopyJobActions.pause, status: CopyJobStatusType.InProgress, shouldShowDialog: false },
|
||||
{ action: CopyJobActions.resume, status: CopyJobStatusType.Paused, shouldShowDialog: false },
|
||||
];
|
||||
|
||||
dialogTests.forEach(({ action, status, mode = CopyJobMigrationType.Offline, shouldShowDialog }, index) => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const job = createMockJob({ Status: status, Mode: mode, Name: `DialogTestJob${index}` });
|
||||
const { unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const actionText = action.charAt(0).toUpperCase() + action.slice(1);
|
||||
if (screen.queryByText(actionText)) {
|
||||
fireEvent.click(screen.getByText(actionText));
|
||||
|
||||
if (shouldShowDialog) {
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
|
||||
expect(mockHandleClick).toHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("should verify component handles state updates correctly", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const stateUpdater = jest.fn();
|
||||
|
||||
const testHandleClick: HandleJobActionClickType = (job, action, setUpdatingJobAction) => {
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
stateUpdater(job.Name, action);
|
||||
};
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={testHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(stateUpdater).toHaveBeenCalledWith(job.Name, CopyJobActions.pause);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Full Integration Coverage", () => {
|
||||
it("should test complete workflow for cancel action with dialog", () => {
|
||||
const job = createMockJob({ Name: "Integration Test Job", Status: CopyJobStatusType.InProgress });
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toHaveAttribute("data-test", "CopyJobActionMenu/Button:Integration Test Job");
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action", // title
|
||||
null, // subText
|
||||
"Confirm", // confirmLabel
|
||||
expect.any(Function), // onOk
|
||||
"Cancel", // cancelLabel
|
||||
null, // onCancel
|
||||
expect.any(Object), // contentHtml (dialogBody)
|
||||
);
|
||||
|
||||
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should test complete workflow for complete action with dialog", () => {
|
||||
const job = createMockJob({
|
||||
Name: "Online Integration Job",
|
||||
Status: CopyJobStatusType.Running,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
|
||||
|
||||
const dialogContent = mockShowOkCancelModalDialog.mock.calls[0][6];
|
||||
expect(dialogContent).toBeTruthy();
|
||||
|
||||
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should maintain proper component lifecycle", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const { rerender, unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
rerender(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
expect(screen.getByRole("button", { name: "Actions" })).toBeInTheDocument();
|
||||
|
||||
expect(() => unmount()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
|
||||
import { IconButton, IContextualMenuProps } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { useDialog } from "../../../Controls/Dialog";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
@@ -10,28 +9,6 @@ interface CopyJobActionMenuProps {
|
||||
handleClick: HandleJobActionClickType;
|
||||
}
|
||||
|
||||
const dialogBody = {
|
||||
[CopyJobActions.cancel]: (jobName: string) => (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
You are about to cancel <b>{jobName}</b> copy job.
|
||||
</Stack.Item>
|
||||
<Stack.Item>Cancelling will stop the job immediately.</Stack.Item>
|
||||
</Stack>
|
||||
),
|
||||
[CopyJobActions.complete]: (jobName: string) => (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
You are about to complete <b>{jobName}</b> copy job.
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
Once completed, continuous data copy will stop after any pending documents are processed. To maintain data
|
||||
integrity, we recommend stopping updates to the source container before completing the job.
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
|
||||
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
||||
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
||||
if (
|
||||
@@ -45,20 +22,6 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
return null;
|
||||
}
|
||||
|
||||
const showActionConfirmationDialog = (job: CopyJobType, action: CopyJobActions): void => {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
ContainerCopyMessages.MonitorJobs.dialog.heading,
|
||||
null,
|
||||
ContainerCopyMessages.MonitorJobs.dialog.confirmButtonText,
|
||||
() => handleClick(job, action, setUpdatingJobAction),
|
||||
ContainerCopyMessages.MonitorJobs.dialog.cancelButtonText,
|
||||
null,
|
||||
action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null,
|
||||
);
|
||||
};
|
||||
|
||||
const getMenuItems = (): IContextualMenuProps["items"] => {
|
||||
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
||||
const updatingAction = updatingJobAction?.action;
|
||||
@@ -69,21 +32,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
||||
iconProps: { iconName: "Pause" },
|
||||
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating,
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.pause,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.cancel,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
||||
iconProps: { iconName: "Cancel" },
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
|
||||
disabled: isThisJobUpdating,
|
||||
onClick: () => handleClick(job, CopyJobActions.cancel, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.cancel,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.resume,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
||||
iconProps: { iconName: "Play" },
|
||||
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating,
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.resume,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -104,7 +67,7 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
key: CopyJobActions.complete,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||
iconProps: { iconName: "CheckMark" },
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
||||
onClick: () => handleClick(job, CopyJobActions.complete, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete,
|
||||
});
|
||||
}
|
||||
@@ -123,8 +86,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
data-test={`CopyJobActionMenu/Button:${job.Name}`}
|
||||
role="button"
|
||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
|
||||
menuIconProps={{ iconName: "", className: "hidden" }}
|
||||
menuProps={{ items: getMenuItems() }}
|
||||
menuIconProps={{ iconName: "" }}
|
||||
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
/>
|
||||
|
||||
@@ -128,7 +128,6 @@ const App = (): JSX.Element => {
|
||||
<>
|
||||
<ContainerCopyPanel explorer={explorer} />
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
</>
|
||||
) : (
|
||||
<DivExplorer explorer={explorer} />
|
||||
|
||||
163
test/E2E-SETUP.md
Normal file
163
test/E2E-SETUP.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# E2E Testing Setup for Cosmos DB Explorer
|
||||
|
||||
This document describes the pre-flight setup process for running end-to-end tests in the Cosmos DB Explorer project.
|
||||
|
||||
## Overview
|
||||
|
||||
The testing setup includes:
|
||||
1. Azure CLI authentication
|
||||
2. Subscription selection
|
||||
3. Test account configuration
|
||||
4. Token generation for NoSQL container operations
|
||||
5. Browser storage state setup for Playwright
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running tests, ensure you have:
|
||||
|
||||
- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) installed and updated
|
||||
- PowerShell execution policy set to allow script execution
|
||||
- Access to the Azure subscription: `074d02eb-4d74-486a-b299-b262264d1536`
|
||||
- Access to the resource group: `bchoudhury-e2e-testing`
|
||||
- Required Azure resources with prefix: `bchoudhury-e2e-`
|
||||
|
||||
## Automatic Setup (Recommended)
|
||||
|
||||
The setup runs automatically when you start Playwright tests:
|
||||
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
The global setup script (`test/global-setup.ts`) will:
|
||||
1. Check if you're logged in to Azure CLI
|
||||
2. If not logged in, prompt for authentication
|
||||
3. Set the correct subscription
|
||||
4. Run the PowerShell configuration script
|
||||
5. Generate required access tokens
|
||||
6. Set up browser authentication state
|
||||
|
||||
## Manual Setup
|
||||
|
||||
If you need to run the setup independently:
|
||||
|
||||
```bash
|
||||
npm run test:e2e:setup
|
||||
```
|
||||
|
||||
or directly:
|
||||
|
||||
```bash
|
||||
node setup-tests.js
|
||||
```
|
||||
|
||||
## Manual Step-by-Step Setup
|
||||
|
||||
If you prefer to run each step manually:
|
||||
|
||||
### 1. Azure CLI Login
|
||||
```bash
|
||||
az login --scope https://management.core.windows.net//.default
|
||||
```
|
||||
|
||||
### 2. Set Subscription
|
||||
```bash
|
||||
az account set --subscription "074d02eb-4d74-486a-b299-b262264d1536"
|
||||
```
|
||||
|
||||
### 3. Configure Test Accounts
|
||||
```powershell
|
||||
.\test\scripts\set-test-accounts.ps1 -ResourceGroup "bchoudhury-e2e-testing" -Subscription "074d02eb-4d74-486a-b299-b262264d1536" -ResourcePrefix "bchoudhury-e2e-"
|
||||
```
|
||||
|
||||
### 4. Set NoSQL Container Token
|
||||
```bash
|
||||
$ENV:NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://bchoudhury-e2e-sqlcontainercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The setup process configures these environment variables:
|
||||
|
||||
- `DE_TEST_RESOURCE_GROUP`: The Azure resource group for testing
|
||||
- `DE_TEST_SUBSCRIPTION_ID`: The Azure subscription ID
|
||||
- `DE_TEST_ACCOUNT_PREFIX`: The prefix used for test resources
|
||||
- `NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN`: Access token for NoSQL operations
|
||||
|
||||
## Storage State
|
||||
|
||||
Browser authentication state is saved to:
|
||||
```
|
||||
playwright/.auth/user.json
|
||||
```
|
||||
|
||||
This file is automatically generated and excluded from version control.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Azure CLI not found**
|
||||
- Install Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
|
||||
|
||||
2. **PowerShell execution policy error**
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
|
||||
3. **Authentication expired**
|
||||
```bash
|
||||
az logout
|
||||
az login --scope https://management.core.windows.net//.default
|
||||
```
|
||||
|
||||
4. **Resource not found**
|
||||
- Verify you have access to the subscription and resource group
|
||||
- Check that resources exist with the expected prefix
|
||||
|
||||
### Debug Mode
|
||||
|
||||
For verbose output during setup, you can run:
|
||||
|
||||
```bash
|
||||
DEBUG=1 npm run test:e2e:setup
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
test/
|
||||
├── global-setup.ts # Main global setup script
|
||||
├── scripts/
|
||||
│ ├── set-test-accounts.ps1 # PowerShell test account setup
|
||||
│ ├── check-test-accounts.ps1
|
||||
│ └── clean-test-accounts.ps1
|
||||
└── ...
|
||||
|
||||
playwright/
|
||||
└── .auth/
|
||||
└── user.json # Browser storage state (generated)
|
||||
|
||||
setup-tests.js # Manual setup runner
|
||||
playwright.config.ts # Playwright config with global setup
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The setup is configured in:
|
||||
- `playwright.config.ts`: Playwright configuration with global setup
|
||||
- `test/global-setup.ts`: Main setup logic
|
||||
- `setup-tests.js`: Manual setup runner
|
||||
|
||||
## Notes
|
||||
|
||||
- The setup only needs to run once per session
|
||||
- Browser storage state persists between test runs
|
||||
- Azure tokens are refreshed automatically by the Azure CLI
|
||||
- The setup is idempotent - safe to run multiple times
|
||||
@@ -701,6 +701,7 @@ export class ContainerCopy {
|
||||
|
||||
static async open(page: Page, testAccount: TestAccount, iframeSrc?: string): Promise<ContainerCopy> {
|
||||
const url = await getTestExplorerUrl(testAccount, { iframeSrc, enablecontainercopy: true });
|
||||
// console.log(`Navigating to URL: ${url}`);
|
||||
await page.goto(url);
|
||||
return ContainerCopy.waitForContainerCopy(page);
|
||||
}
|
||||
|
||||
145
test/global-setup copy.js
Normal file
145
test/global-setup copy.js
Normal file
@@ -0,0 +1,145 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
const { exec } = require('child_process');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function globalSetup(config) {
|
||||
console.log('🚀 Starting global setup for Azure Cosmos DB Explorer tests...');
|
||||
|
||||
try {
|
||||
// Step 1: Check if user is already logged in to Azure CLI
|
||||
console.log('📋 Checking Azure CLI login status...');
|
||||
try {
|
||||
const { stdout: accountInfo } = await execAsync('az account show --output json');
|
||||
const account = JSON.parse(accountInfo);
|
||||
console.log(`✅ Already logged in as: ${account.user?.name} in subscription: ${account.name}`);
|
||||
} catch (error) {
|
||||
console.log('🔐 Not logged in to Azure CLI, initiating login...');
|
||||
|
||||
// Step 2: Login to Azure with specific scope
|
||||
console.log('🔑 Logging in to Azure CLI...');
|
||||
console.log('Please complete the authentication in your browser...');
|
||||
|
||||
await execAsync('az login --scope https://management.core.windows.net//.default');
|
||||
console.log('✅ Azure CLI login completed');
|
||||
}
|
||||
|
||||
// Step 3: Set the subscription
|
||||
const targetSubscription = '074d02eb-4d74-486a-b299-b262264d1536';
|
||||
console.log(`🔄 Setting subscription to: ${targetSubscription}`);
|
||||
|
||||
await execAsync(`az account set --subscription "${targetSubscription}"`);
|
||||
console.log('✅ Subscription set successfully');
|
||||
|
||||
// Step 4: Run the PowerShell test account setup script
|
||||
console.log('⚙️ Running test account setup script...');
|
||||
const scriptPath = path.join(__dirname, 'scripts', 'set-test-accounts.ps1');
|
||||
const resourceGroup = 'bchoudhury-e2e-testing';
|
||||
const resourcePrefix = 'bchoudhury-e2e-';
|
||||
|
||||
// Try PowerShell 7 first, fallback to Windows PowerShell if not available
|
||||
let psCommand = `pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -ResourceGroup "${resourceGroup}" -Subscription "${targetSubscription}" -ResourcePrefix "${resourcePrefix}"`;
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(psCommand);
|
||||
if (stdout) console.log('PowerShell Output:', stdout);
|
||||
if (stderr) console.log('PowerShell Warnings:', stderr);
|
||||
console.log('✅ Test account setup completed');
|
||||
} catch (psError) {
|
||||
console.log('🔄 PowerShell 7 not available, trying Windows PowerShell...');
|
||||
|
||||
// Fallback to Windows PowerShell with NoProfile to avoid module loading issues
|
||||
const fallbackCommand = `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -ResourceGroup "${resourceGroup}" -Subscription "${targetSubscription}" -ResourcePrefix "${resourcePrefix}"`;
|
||||
|
||||
try {
|
||||
const { stdout: fbStdout, stderr: fbStderr } = await execAsync(fallbackCommand);
|
||||
if (fbStdout) console.log('PowerShell Output:', fbStdout);
|
||||
if (fbStderr) console.log('PowerShell Warnings:', fbStderr);
|
||||
console.log('✅ Test account setup completed');
|
||||
} catch (fallbackError) {
|
||||
console.error('❌ PowerShell script execution failed:', fallbackError.message);
|
||||
console.warn('⚠️ The PowerShell script failed. This might be due to:');
|
||||
console.warn(' 1. PowerShell version compatibility (script requires PowerShell 7+ for ?? operator)');
|
||||
console.warn(' 2. Missing Azure PowerShell modules');
|
||||
console.warn(' 3. Insufficient permissions or missing resources');
|
||||
// Don't throw here as the script might have partial success
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Set the NoSQL container copy test account token
|
||||
console.log('🎟️ Setting up NoSQL container copy test account token...');
|
||||
try {
|
||||
const { stdout: tokenOutput } = await execAsync(
|
||||
'az account get-access-token --scope "https://bchoudhury-e2e-sqlcontainercopyonly.documents.azure.com/.default" -o tsv --query accessToken'
|
||||
);
|
||||
|
||||
const token = tokenOutput.trim();
|
||||
if (token) {
|
||||
process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN = token;
|
||||
console.log('✅ NoSQL container copy token set successfully');
|
||||
} else {
|
||||
console.warn('⚠️ Failed to retrieve NoSQL container copy token');
|
||||
}
|
||||
} catch (tokenError) {
|
||||
console.error('❌ Failed to set NoSQL token:', tokenError.message);
|
||||
// Continue without throwing as this might not be critical for all tests
|
||||
}
|
||||
|
||||
// Step 6: Create browser context and save storage state for authentication
|
||||
console.log('🌐 Setting up browser authentication state...');
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
// Navigate to a login page or perform any authentication steps if needed
|
||||
// For now, we'll just create an empty storage state that can be populated later
|
||||
|
||||
// Save the storage state
|
||||
const storageStatePath = path.join(__dirname, '../playwright/.auth/user.json');
|
||||
await fs.mkdir(path.dirname(storageStatePath), { recursive: true });
|
||||
await context.storageState({ path: storageStatePath });
|
||||
|
||||
await browser.close();
|
||||
|
||||
console.log(`✅ Browser storage state saved to: ${storageStatePath}`);
|
||||
|
||||
// Step 7: Set environment variables that tests might need
|
||||
console.log('📝 Setting up environment variables for tests...');
|
||||
|
||||
// Verify required environment variables are set
|
||||
const requiredEnvVars = [
|
||||
'DE_TEST_RESOURCE_GROUP',
|
||||
'DE_TEST_SUBSCRIPTION_ID',
|
||||
'DE_TEST_ACCOUNT_PREFIX'
|
||||
];
|
||||
|
||||
const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
|
||||
if (missingVars.length > 0) {
|
||||
console.warn(`⚠️ Missing environment variables: ${missingVars.join(', ')}`);
|
||||
console.warn('These should be set by the PowerShell script. Tests may fail.');
|
||||
} else {
|
||||
console.log('✅ All required environment variables are set');
|
||||
}
|
||||
|
||||
console.log('🎉 Global setup completed successfully!');
|
||||
console.log('\n📊 Setup Summary:');
|
||||
console.log(` - Subscription: ${targetSubscription}`);
|
||||
console.log(` - Resource Group: ${resourceGroup}`);
|
||||
console.log(` - Resource Prefix: ${resourcePrefix}`);
|
||||
console.log(` - Storage State: ${storageStatePath}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Global setup failed:', error.message);
|
||||
console.error('Stack trace:', error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = globalSetup;
|
||||
135
test/global-setup.ts
Normal file
135
test/global-setup.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { type FullConfig } from '@playwright/test';
|
||||
import { exec } from 'child_process';
|
||||
import path from 'path';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export default async function globalSetup(
|
||||
_config?: FullConfig
|
||||
): Promise<void> {
|
||||
console.log('🚀 Starting global setup for Azure Cosmos DB Explorer tests...');
|
||||
|
||||
try {
|
||||
// ------------------------------------------------------------------
|
||||
// Step 1: Azure CLI login
|
||||
// ------------------------------------------------------------------
|
||||
console.log('📋 Checking Azure CLI login status...');
|
||||
try {
|
||||
const { stdout } = await execAsync('az account show --output json');
|
||||
const account = JSON.parse(stdout);
|
||||
console.log(
|
||||
`✅ Already logged in as: ${account.user?.name} in subscription: ${account.name}`
|
||||
);
|
||||
} catch {
|
||||
console.log('🔐 Not logged in to Azure CLI, initiating login...');
|
||||
await execAsync(
|
||||
'az login --scope https://management.core.windows.net//.default'
|
||||
);
|
||||
console.log('✅ Azure CLI login completed');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Step 2: Set subscription
|
||||
// ------------------------------------------------------------------
|
||||
const targetSubscription = '074d02eb-4d74-486a-b299-b262264d1536';
|
||||
console.log(`🔄 Setting subscription to: ${targetSubscription}`);
|
||||
await execAsync(
|
||||
`az account set --subscription "${targetSubscription}"`
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Step 3: Run PowerShell test account setup (DOT-SOURCED)
|
||||
// ------------------------------------------------------------------
|
||||
console.log('⚙️ Running test account setup script...');
|
||||
|
||||
const scriptPath = path.join(
|
||||
__dirname,
|
||||
'scripts',
|
||||
'set-test-accounts.ps1'
|
||||
);
|
||||
|
||||
const resourceGroup = 'bchoudhury-e2e-testing';
|
||||
const resourcePrefix = 'bchoudhury-e2e-';
|
||||
|
||||
// IMPORTANT:
|
||||
// - Dot-source the PS1
|
||||
// - Print env vars BEFORE PowerShell exits
|
||||
const psScript = `
|
||||
. "${scriptPath}" `
|
||||
+ `-ResourceGroup "${resourceGroup}" `
|
||||
+ `-Subscription "${targetSubscription}" `
|
||||
+ `-ResourcePrefix "${resourcePrefix}"
|
||||
|
||||
Get-ChildItem Env:DE_TEST_* | ForEach-Object {
|
||||
Write-Output "$($_.Name)=$($_.Value)"
|
||||
}
|
||||
`;
|
||||
|
||||
// PowerShell requires UTF-16LE encoding for -EncodedCommand
|
||||
const encodedCommand = Buffer.from(psScript, 'utf16le').toString('base64');
|
||||
|
||||
const { stdout: psStdout, stderr: psStderr } = await execAsync(
|
||||
`pwsh -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`
|
||||
);
|
||||
|
||||
if (psStderr) {
|
||||
console.log('PowerShell warnings:', psStderr);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Step 3a: Import env vars from PowerShell
|
||||
// ------------------------------------------------------------------
|
||||
psStdout
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.startsWith('DE_TEST_'))
|
||||
.forEach(line => {
|
||||
const [key, ...rest] = line.split('=');
|
||||
process.env[key] = rest.join('=');
|
||||
console.log(`✅ Imported env var from PS: ${key}`);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Step 4: NoSQL access token
|
||||
// ------------------------------------------------------------------
|
||||
console.log('🎟️ Setting up NoSQL container copy test account token...');
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
'az account get-access-token --scope "https://bchoudhury-e2e-sqlcontainercopyonly.documents.azure.com/.default" -o tsv --query accessToken'
|
||||
);
|
||||
const token = stdout.trim();
|
||||
if (token) {
|
||||
process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN = token;
|
||||
console.log('✅ NoSQL container copy token set successfully');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('❌ Failed to set NoSQL token:', err.message);
|
||||
}
|
||||
|
||||
// Browser authentication not needed - using API tokens directly
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Step 6: Validate env vars
|
||||
// ------------------------------------------------------------------
|
||||
console.log('📝 Validating environment variables...');
|
||||
const requiredEnvVars = [
|
||||
'DE_TEST_RESOURCE_GROUP',
|
||||
'DE_TEST_SUBSCRIPTION_ID',
|
||||
'DE_TEST_ACCOUNT_PREFIX',
|
||||
];
|
||||
|
||||
const missing = requiredEnvVars.filter(v => !process.env[v]);
|
||||
if (missing.length > 0) {
|
||||
console.warn(`⚠️ Missing environment variables: ${missing.join(', ')}`);
|
||||
} else {
|
||||
console.log('✅ All required environment variables are set');
|
||||
}
|
||||
|
||||
console.log('🎉 Global setup completed successfully!');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Global setup failed:', error.message);
|
||||
console.error(error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,10 @@ if (-not $Subscription) {
|
||||
$Subscription = $currentSubscription.Id
|
||||
}
|
||||
|
||||
$AzSubscription = (Get-AzSubscription -SubscriptionId $Subscription -ErrorAction SilentlyContinue | Select-Object -First 1) ?? (Get-AzSubscription -SubscriptionName $Subscription -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
$AzSubscription = (Get-AzSubscription -SubscriptionId $Subscription -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
if (-not $AzSubscription) {
|
||||
$AzSubscription = (Get-AzSubscription -SubscriptionName $Subscription -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
}
|
||||
if (-not $AzSubscription) {
|
||||
throw "The subscription '$Subscription' could not be found."
|
||||
}
|
||||
|
||||
7
test/seed.spec.ts
Normal file
7
test/seed.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Test group', () => {
|
||||
test('seed', async ({ page }) => {
|
||||
// generate code here.
|
||||
});
|
||||
});
|
||||
51
test/setup-verification.spec.ts
Normal file
51
test/setup-verification.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Azure Setup Verification', () => {
|
||||
test('should have required environment variables set', async ({ page }) => {
|
||||
// Verify that the global setup has properly configured the environment
|
||||
const requiredEnvVars = [
|
||||
'DE_TEST_RESOURCE_GROUP',
|
||||
'DE_TEST_SUBSCRIPTION_ID',
|
||||
'DE_TEST_ACCOUNT_PREFIX'
|
||||
];
|
||||
|
||||
for (const envVar of requiredEnvVars) {
|
||||
expect(process.env[envVar], `Environment variable ${envVar} should be set by global setup`).toBeDefined();
|
||||
expect(process.env[envVar], `Environment variable ${envVar} should not be empty`).not.toBe('');
|
||||
}
|
||||
|
||||
console.log('✅ Environment Variables:');
|
||||
console.log(` DE_TEST_RESOURCE_GROUP: ${process.env.DE_TEST_RESOURCE_GROUP}`);
|
||||
console.log(` DE_TEST_SUBSCRIPTION_ID: ${process.env.DE_TEST_SUBSCRIPTION_ID}`);
|
||||
console.log(` DE_TEST_ACCOUNT_PREFIX: ${process.env.DE_TEST_ACCOUNT_PREFIX}`);
|
||||
|
||||
if (process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN) {
|
||||
console.log(` NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN: [SET]`);
|
||||
} else {
|
||||
console.log(` NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN: [NOT SET]`);
|
||||
}
|
||||
});
|
||||
|
||||
/* test('should be able to navigate to the application', async ({ page }) => {
|
||||
// This assumes the web server is running on https://127.0.0.1:1234
|
||||
// as configured in playwright.config.ts
|
||||
try {
|
||||
await page.goto('https://127.0.0.1:1234');
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if the page loaded successfully
|
||||
const title = await page.title();
|
||||
console.log(`Page title: ${title}`);
|
||||
|
||||
// This is a basic check - you might want to check for specific elements
|
||||
// that indicate the Cosmos DB Explorer has loaded correctly
|
||||
expect(title).toBeTruthy();
|
||||
|
||||
} catch (error) {
|
||||
console.log('Note: Application server might not be running. Start it with: npm run start');
|
||||
throw error;
|
||||
}
|
||||
}); */
|
||||
});
|
||||
505
test/sql/containercopy.spec.ts
Normal file
505
test/sql/containercopy.spec.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { expect, Frame, Locator, Page, test } from "@playwright/test";
|
||||
import { set } from "lodash";
|
||||
import { truncateName } from "../../src/Explorer/ContainerCopy/CopyJobUtils";
|
||||
import {
|
||||
ContainerCopy,
|
||||
getAccountName,
|
||||
getDropdownItemByNameOrPosition,
|
||||
interceptAndInspectApiRequest,
|
||||
TestAccount,
|
||||
waitForApiResponse,
|
||||
} from "../fx";
|
||||
import { createMultipleTestContainers } from "../testData";
|
||||
|
||||
let page: Page;
|
||||
let wrapper: Locator = null!;
|
||||
let panel: Locator = null!;
|
||||
let frame: Frame = null!;
|
||||
let expectedCopyJobNameInitial: string = null!;
|
||||
let expectedJobName: string = "";
|
||||
let targetAccountName: string = "";
|
||||
let expectedSourceAccountName: string = "";
|
||||
let expectedSubscriptionName: string = "";
|
||||
const VISIBLE_TIMEOUT_MS = 30 * 1000;
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.describe("Container Copy", () => {
|
||||
test.beforeAll("Container Copy - Before All", async ({ browser }) => {
|
||||
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 3 });
|
||||
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
expectedJobName = `test_job_${Date.now()}`;
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
});
|
||||
|
||||
test.afterEach("Container Copy - After Each", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
});
|
||||
|
||||
test("Loading and verifying the content of the page", async () => {
|
||||
expect(wrapper).not.toBeNull();
|
||||
await expect(wrapper.getByTestId("CommandBar/Button:Create Copy Job")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
});
|
||||
|
||||
test("Successfully create a copy job for offline migration", async () => {
|
||||
expect(wrapper).not.toBeNull();
|
||||
// Loading and verifying subscription & account dropdown
|
||||
|
||||
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||
await createCopyJobButton.click();
|
||||
panel = frame.getByTestId("Panel:Create copy job");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(10 * 1000);
|
||||
|
||||
const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
|
||||
|
||||
const expectedAccountName = targetAccountName;
|
||||
expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText();
|
||||
|
||||
await subscriptionDropdown.click();
|
||||
const subscriptionItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ name: expectedSubscriptionName },
|
||||
{ ariaLabel: "Subscription" },
|
||||
);
|
||||
await subscriptionItem.click();
|
||||
|
||||
// Load account dropdown based on selected subscription
|
||||
|
||||
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||
await expect(accountDropdown).toHaveText(new RegExp(expectedAccountName));
|
||||
await accountDropdown.click();
|
||||
|
||||
const accountItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ name: expectedAccountName },
|
||||
{ ariaLabel: "Account" },
|
||||
);
|
||||
await accountItem.click();
|
||||
|
||||
// Verifying online or offline migration functionality
|
||||
/**
|
||||
* This test verifies the functionality of the migration type radio that toggles between
|
||||
* online and offline container copy modes. It ensures that:
|
||||
* 1. When online mode is selected, the user is directed to a permissions screen
|
||||
* 2. When offline mode is selected, the user bypasses the permissions screen
|
||||
* 3. The UI correctly reflects the selected migration type throughout the workflow
|
||||
*/
|
||||
const migrationTypeContainer = panel.getByTestId("migration-type");
|
||||
const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i });
|
||||
await onlineCopyRadioButton.click({ force: true });
|
||||
|
||||
await expect(migrationTypeContainer.getByTestId("migration-type-description-online")).toBeVisible();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).toBeVisible();
|
||||
await expect(panel.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||
await panel.getByRole("button", { name: "Previous" }).click();
|
||||
|
||||
const offlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Offline mode/i });
|
||||
await offlineCopyRadioButton.click({ force: true });
|
||||
|
||||
await expect(migrationTypeContainer.getByTestId("migration-type-description-offline")).toBeVisible();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await expect(panel.getByTestId("Panel:SelectSourceAndTargetContainers")).toBeVisible();
|
||||
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).not.toBeVisible();
|
||||
|
||||
// Verifying source and target container selection
|
||||
|
||||
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
|
||||
expect(sourceContainerDropdown).toBeVisible();
|
||||
await expect(sourceContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
|
||||
await sourceDatabaseDropdown.click();
|
||||
|
||||
const sourceDbDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Database" },
|
||||
);
|
||||
await sourceDbDropdownItem.click();
|
||||
|
||||
await expect(sourceContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
await sourceContainerDropdown.click();
|
||||
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Container" },
|
||||
);
|
||||
await sourceContainerDropdownItem.click();
|
||||
|
||||
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
|
||||
expect(targetContainerDropdown).toBeVisible();
|
||||
await expect(targetContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
|
||||
await targetDatabaseDropdown.click();
|
||||
const targetDbDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Database" },
|
||||
);
|
||||
await targetDbDropdownItem.click();
|
||||
|
||||
await expect(targetContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
await targetContainerDropdown.click();
|
||||
const targetContainerDropdownItem1 = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Container" },
|
||||
);
|
||||
await targetContainerDropdownItem1.click();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
|
||||
await expect(errorContainer).toBeVisible();
|
||||
await expect(errorContainer).toHaveText(/Source and destination containers cannot be the same/i);
|
||||
|
||||
// Reselect target container to be different from source container
|
||||
await targetContainerDropdown.click();
|
||||
const targetContainerDropdownItem2 = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 1 },
|
||||
{ ariaLabel: "Container" },
|
||||
);
|
||||
await targetContainerDropdownItem2.click();
|
||||
|
||||
const selectedSourceDatabase = await sourceDatabaseDropdown.innerText();
|
||||
const selectedSourceContainer = await sourceContainerDropdown.innerText();
|
||||
const selectedTargetDatabase = await targetDatabaseDropdown.innerText();
|
||||
const selectedTargetContainer = await targetContainerDropdown.innerText();
|
||||
expectedCopyJobNameInitial = `${truncateName(selectedSourceDatabase)}.${truncateName(
|
||||
selectedSourceContainer,
|
||||
)}_${truncateName(selectedTargetDatabase)}.${truncateName(selectedTargetContainer)}`;
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await expect(errorContainer).not.toBeVisible();
|
||||
await expect(panel.getByTestId("Panel:PreviewCopyJob")).toBeVisible();
|
||||
|
||||
// Verifying the preview of the copy job
|
||||
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
|
||||
await expect(previewContainer).toBeVisible();
|
||||
await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(expectedSubscriptionName);
|
||||
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedAccountName);
|
||||
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
|
||||
await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial));
|
||||
const primaryBtn = panel.getByRole("button", { name: "Copy", exact: true });
|
||||
await expect(primaryBtn).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
await jobNameInput.fill("test job name");
|
||||
await expect(primaryBtn).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
// Testing API request interception with duplicate job name
|
||||
const duplicateJobName = "test-job-name-1";
|
||||
await jobNameInput.fill(duplicateJobName);
|
||||
|
||||
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
|
||||
const expectedErrorMessage = `Duplicate job name '${duplicateJobName}'`;
|
||||
await interceptAndInspectApiRequest(
|
||||
page,
|
||||
`${expectedAccountName}/dataTransferJobs/${duplicateJobName}`,
|
||||
"PUT",
|
||||
new Error(expectedErrorMessage),
|
||||
(url?: string) => url?.includes(duplicateJobName) ?? false,
|
||||
);
|
||||
|
||||
let errorThrown = false;
|
||||
try {
|
||||
await copyButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
} catch (error: any) {
|
||||
errorThrown = true;
|
||||
expect(error.message).toContain("not allowed");
|
||||
}
|
||||
if (!errorThrown) {
|
||||
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
|
||||
await expect(errorContainer).toBeVisible();
|
||||
await expect(errorContainer).toHaveText(new RegExp(expectedErrorMessage, "i"));
|
||||
}
|
||||
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
// Testing API request success with valid job name and verifying copy job creation
|
||||
|
||||
const validJobName = expectedJobName;
|
||||
|
||||
const copyJobCreationPromise = waitForApiResponse(
|
||||
page,
|
||||
`${expectedAccountName}/dataTransferJobs/${validJobName}`,
|
||||
"PUT",
|
||||
);
|
||||
|
||||
await jobNameInput.fill(validJobName);
|
||||
await expect(copyButton).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
await copyButton.click();
|
||||
|
||||
const response = await copyJobCreationPromise;
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
await expect(panel).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||
await jobsListContainer.waitFor({ state: "visible" });
|
||||
|
||||
const jobItem = jobsListContainer.getByText(validJobName);
|
||||
await jobItem.waitFor({ state: "visible" });
|
||||
await expect(jobItem).toBeVisible();
|
||||
});
|
||||
|
||||
test("Verify Online or Offline Container Copy Permissions Panel", async () => {
|
||||
expect(wrapper).not.toBeNull();
|
||||
|
||||
// Opening the Create Copy Job panel again to verify initial state
|
||||
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||
await createCopyJobButton.click();
|
||||
panel = frame.getByTestId("Panel:Create copy job");
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(panel.getByRole("heading", { name: "Create copy job" })).toBeVisible();
|
||||
|
||||
// select different account dropdown
|
||||
|
||||
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||
await accountDropdown.click();
|
||||
|
||||
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
|
||||
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual("Account");
|
||||
|
||||
const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all();
|
||||
|
||||
const filteredItems = [];
|
||||
for (const item of allDropdownItems) {
|
||||
const testContent = (await item.textContent()) ?? "";
|
||||
if (testContent.trim() !== targetAccountName.trim()) {
|
||||
filteredItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
const firstDropdownItem = filteredItems[0];
|
||||
expectedSourceAccountName = (await firstDropdownItem.textContent()) ?? "";
|
||||
await firstDropdownItem.click();
|
||||
} else {
|
||||
throw new Error("No dropdown items available after filtering");
|
||||
}
|
||||
|
||||
const migrationTypeContainer = panel.getByTestId("migration-type");
|
||||
const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i });
|
||||
await onlineCopyRadioButton.click({ force: true });
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Verifying Assign Permissions panel for online copy
|
||||
|
||||
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
|
||||
await expect(permissionScreen).toBeVisible();
|
||||
|
||||
await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||
await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible();
|
||||
|
||||
// Verify Point-in-Time Restore timer and refresh button workflow
|
||||
|
||||
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}**`, async (route) => {
|
||||
const mockData = {
|
||||
identity: {
|
||||
type: "SystemAssigned",
|
||||
principalId: "00-11-22-33",
|
||||
},
|
||||
properties: {
|
||||
defaultIdentity: "SystemAssignedIdentity",
|
||||
backupPolicy: {
|
||||
type: "Continuous",
|
||||
},
|
||||
capabilities: [{ name: "EnableOnlineContainerCopy" }],
|
||||
},
|
||||
};
|
||||
if (route.request().method() === "GET") {
|
||||
const response = await route.fetch();
|
||||
const actualData = await response.json();
|
||||
const mergedData = { ...actualData };
|
||||
|
||||
set(mergedData, "identity", mockData.identity);
|
||||
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
|
||||
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
|
||||
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(mergedData),
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await expect(permissionScreen).toBeVisible();
|
||||
|
||||
const expandedOnlineAccordionHeader = permissionScreen
|
||||
.getByTestId("permission-group-container-onlineConfigs")
|
||||
.locator("button[aria-expanded='true']");
|
||||
await expect(expandedOnlineAccordionHeader).toBeVisible();
|
||||
|
||||
const accordionItem = expandedOnlineAccordionHeader
|
||||
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
|
||||
.first();
|
||||
|
||||
const accordionPanel = accordionItem
|
||||
.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']")
|
||||
.first();
|
||||
|
||||
await page.clock.install({ time: new Date("2024-01-01T10:00:00Z") });
|
||||
|
||||
const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn");
|
||||
await expect(pitrBtn).toBeVisible();
|
||||
await pitrBtn.click();
|
||||
|
||||
page.context().on("page", async (newPage) => {
|
||||
const expectedUrlEndPattern = new RegExp(
|
||||
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${expectedSourceAccountName}/backupRestore`,
|
||||
);
|
||||
expect(newPage.url()).toMatch(expectedUrlEndPattern);
|
||||
await newPage.close();
|
||||
});
|
||||
|
||||
const loadingOverlay = frame.locator("[data-test='loading-overlay']");
|
||||
await expect(loadingOverlay).toBeVisible();
|
||||
|
||||
const refreshBtn = accordionPanel.getByTestId("pointInTimeRestore:RefreshBtn");
|
||||
await expect(refreshBtn).not.toBeVisible();
|
||||
|
||||
// Fast forward time by 11 minutes (11 * 60 * 1000ms = 660000ms)
|
||||
await page.clock.fastForward(11 * 60 * 1000);
|
||||
|
||||
await expect(refreshBtn).toBeVisible();
|
||||
await expect(pitrBtn).not.toBeVisible();
|
||||
|
||||
// Veify Popover & Loading Overlay on permission screen with API mocks and accordion interactions
|
||||
|
||||
await page.route(
|
||||
`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/sqlRoleAssignments*`,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
value: [
|
||||
{
|
||||
principalId: "00-11-22-33",
|
||||
roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/77-88-99`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await page.route("**/Microsoft.DocumentDB/databaseAccounts/*/77-88-99**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
value: [
|
||||
{
|
||||
name: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}**`, async (route) => {
|
||||
const mockData = {
|
||||
identity: {
|
||||
type: "SystemAssigned",
|
||||
principalId: "00-11-22-33",
|
||||
},
|
||||
properties: {
|
||||
defaultIdentity: "SystemAssignedIdentity",
|
||||
backupPolicy: {
|
||||
type: "Continuous",
|
||||
},
|
||||
capabilities: [{ name: "EnableOnlineContainerCopy" }],
|
||||
},
|
||||
};
|
||||
|
||||
if (route.request().method() === "PATCH") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ status: "Succeeded" }),
|
||||
});
|
||||
} else if (route.request().method() === "GET") {
|
||||
// Get the actual response and merge with mock data
|
||||
const response = await route.fetch();
|
||||
const actualData = await response.json();
|
||||
const mergedData = { ...actualData };
|
||||
set(mergedData, "identity", mockData.identity);
|
||||
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
|
||||
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
|
||||
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(mergedData),
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await expect(permissionScreen).toBeVisible();
|
||||
|
||||
const expandedCrossAccordionHeader = permissionScreen
|
||||
.getByTestId("permission-group-container-crossAccountConfigs")
|
||||
.locator("button[aria-expanded='true']");
|
||||
await expect(expandedCrossAccordionHeader).toBeVisible();
|
||||
|
||||
const crossAccordionItem = expandedCrossAccordionHeader
|
||||
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
|
||||
.first();
|
||||
|
||||
const crossAccordionPanel = crossAccordionItem
|
||||
.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']")
|
||||
.first();
|
||||
|
||||
const toggleButton = crossAccordionPanel.getByTestId("btn-toggle");
|
||||
await expect(toggleButton).toBeVisible();
|
||||
await toggleButton.click();
|
||||
|
||||
const popover = frame.locator("[data-test='popover-container']");
|
||||
await expect(popover).toBeVisible();
|
||||
|
||||
const yesButton = popover.getByRole("button", { name: /Yes/i });
|
||||
const noButton = popover.getByRole("button", { name: /No/i });
|
||||
await expect(yesButton).toBeVisible();
|
||||
await expect(noButton).toBeVisible();
|
||||
|
||||
await yesButton.click();
|
||||
|
||||
await expect(loadingOverlay).toBeVisible();
|
||||
|
||||
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
|
||||
await expect(popover).toBeHidden({ timeout: 10 * 1000 });
|
||||
|
||||
await panel.getByRole("button", { name: "Cancel" }).click();
|
||||
});
|
||||
|
||||
test.afterAll("Container Copy - After All", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
await page.close();
|
||||
});
|
||||
});
|
||||
@@ -1,258 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { expect, Frame, Locator, Page, test } from "@playwright/test";
|
||||
import { truncateName } from "../../../src/Explorer/ContainerCopy/CopyJobUtils";
|
||||
import {
|
||||
ContainerCopy,
|
||||
getAccountName,
|
||||
getDropdownItemByNameOrPosition,
|
||||
interceptAndInspectApiRequest,
|
||||
TestAccount,
|
||||
waitForApiResponse,
|
||||
} from "../../fx";
|
||||
import { createMultipleTestContainers } from "../../testData";
|
||||
|
||||
test.describe("Container Copy - Offline Migration", () => {
|
||||
let page: Page;
|
||||
let wrapper: Locator;
|
||||
let panel: Locator;
|
||||
let frame: Frame;
|
||||
let expectedJobName: string;
|
||||
let targetAccountName: string;
|
||||
let expectedSubscriptionName: string;
|
||||
let expectedCopyJobNameInitial: string;
|
||||
|
||||
test.beforeEach("Setup for offline migration test", async ({ browser }) => {
|
||||
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
|
||||
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
expectedJobName = `offline_test_job_${Date.now()}`;
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
});
|
||||
|
||||
test.afterEach("Cleanup after offline migration test", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test("Successfully create and manage offline migration copy job", async () => {
|
||||
expect(wrapper).not.toBeNull();
|
||||
await wrapper.locator(".commandBarContainer").waitFor({ state: "visible" });
|
||||
|
||||
// Open Create Copy Job panel
|
||||
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||
await expect(createCopyJobButton).toBeVisible();
|
||||
await createCopyJobButton.click();
|
||||
panel = frame.getByTestId("Panel:Create copy job");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
// Reduced wait time for better performance
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Setup subscription and account
|
||||
const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
|
||||
const expectedAccountName = targetAccountName;
|
||||
expectedSubscriptionName = await subscriptionDropdown.locator("span.ms-Dropdown-title").innerText();
|
||||
|
||||
await subscriptionDropdown.click();
|
||||
const subscriptionItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ name: expectedSubscriptionName },
|
||||
{ ariaLabel: "Subscription" },
|
||||
);
|
||||
await subscriptionItem.click();
|
||||
|
||||
// Select account
|
||||
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||
await expect(accountDropdown).toHaveText(new RegExp(expectedAccountName));
|
||||
await accountDropdown.click();
|
||||
|
||||
const accountItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ name: expectedAccountName },
|
||||
{ ariaLabel: "Account" },
|
||||
);
|
||||
await accountItem.click();
|
||||
|
||||
// Test offline migration mode toggle functionality
|
||||
const migrationTypeContainer = panel.getByTestId("migration-type");
|
||||
|
||||
// First test online mode (should show permissions screen)
|
||||
const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i });
|
||||
await onlineCopyRadioButton.click({ force: true });
|
||||
await expect(migrationTypeContainer.getByTestId("migration-type-description-online")).toBeVisible();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).toBeVisible();
|
||||
await expect(panel.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||
|
||||
// Go back and switch to offline mode
|
||||
await panel.getByRole("button", { name: "Previous" }).click();
|
||||
|
||||
const offlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Offline mode/i });
|
||||
await offlineCopyRadioButton.click({ force: true });
|
||||
await expect(migrationTypeContainer.getByTestId("migration-type-description-offline")).toBeVisible();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Verify we skip permissions screen in offline mode
|
||||
await expect(panel.getByTestId("Panel:SelectSourceAndTargetContainers")).toBeVisible();
|
||||
await expect(panel.getByTestId("Panel:AssignPermissionsContainer")).not.toBeVisible();
|
||||
|
||||
// Test source and target container selection with validation
|
||||
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
|
||||
expect(sourceContainerDropdown).toBeVisible();
|
||||
await expect(sourceContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
// Select source database first (containers are disabled until database is selected)
|
||||
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
|
||||
await sourceDatabaseDropdown.click();
|
||||
const sourceDbDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Database" },
|
||||
);
|
||||
await sourceDbDropdownItem.click();
|
||||
|
||||
// Now container dropdown should be enabled
|
||||
await expect(sourceContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
await sourceContainerDropdown.click();
|
||||
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Container" },
|
||||
);
|
||||
await sourceContainerDropdownItem.click();
|
||||
|
||||
// Test target container selection
|
||||
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
|
||||
expect(targetContainerDropdown).toBeVisible();
|
||||
await expect(targetContainerDropdown).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
|
||||
await targetDatabaseDropdown.click();
|
||||
const targetDbDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Database" },
|
||||
);
|
||||
await targetDbDropdownItem.click();
|
||||
|
||||
await expect(targetContainerDropdown).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
await targetContainerDropdown.click();
|
||||
|
||||
// First try selecting the same container (should show error)
|
||||
const targetContainerDropdownItem1 = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Container" },
|
||||
);
|
||||
await targetContainerDropdownItem1.click();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Verify validation error for same source and target containers
|
||||
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
|
||||
await expect(errorContainer).toBeVisible();
|
||||
await expect(errorContainer).toHaveText(/Source and destination containers cannot be the same/i);
|
||||
|
||||
// Select different target container
|
||||
await targetContainerDropdown.click();
|
||||
const targetContainerDropdownItem2 = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 1 },
|
||||
{ ariaLabel: "Container" },
|
||||
);
|
||||
await targetContainerDropdownItem2.click();
|
||||
|
||||
// Generate expected job name based on selections
|
||||
const selectedSourceDatabase = await sourceDatabaseDropdown.innerText();
|
||||
const selectedSourceContainer = await sourceContainerDropdown.innerText();
|
||||
const selectedTargetDatabase = await targetDatabaseDropdown.innerText();
|
||||
const selectedTargetContainer = await targetContainerDropdown.innerText();
|
||||
expectedCopyJobNameInitial = `${truncateName(selectedSourceDatabase)}.${truncateName(
|
||||
selectedSourceContainer,
|
||||
)}_${truncateName(selectedTargetDatabase)}.${truncateName(selectedTargetContainer)}`;
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Error should disappear and preview should be visible
|
||||
await expect(errorContainer).not.toBeVisible();
|
||||
await expect(panel.getByTestId("Panel:PreviewCopyJob")).toBeVisible();
|
||||
|
||||
// Verify job preview details
|
||||
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
|
||||
await expect(previewContainer).toBeVisible();
|
||||
await expect(previewContainer.getByTestId("source-subscription-name")).toHaveText(expectedSubscriptionName);
|
||||
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(expectedAccountName);
|
||||
|
||||
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
|
||||
await expect(jobNameInput).toHaveValue(new RegExp(expectedCopyJobNameInitial));
|
||||
|
||||
const primaryBtn = panel.getByRole("button", { name: "Copy", exact: true });
|
||||
await expect(primaryBtn).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
// Test invalid job name validation (spaces not allowed)
|
||||
await jobNameInput.fill("test job name");
|
||||
await expect(primaryBtn).toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
// Test duplicate job name error handling
|
||||
const duplicateJobName = "test-job-name-1";
|
||||
await jobNameInput.fill(duplicateJobName);
|
||||
|
||||
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
|
||||
const expectedErrorMessage = `Duplicate job name '${duplicateJobName}'`;
|
||||
|
||||
await interceptAndInspectApiRequest(
|
||||
page,
|
||||
`${expectedAccountName}/dataTransferJobs/${duplicateJobName}`,
|
||||
"PUT",
|
||||
new Error(expectedErrorMessage),
|
||||
(url?: string) => url?.includes(duplicateJobName) ?? false,
|
||||
);
|
||||
|
||||
let errorThrown = false;
|
||||
try {
|
||||
await copyButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
} catch (error: any) {
|
||||
errorThrown = true;
|
||||
expect(error.message).toContain("not allowed");
|
||||
}
|
||||
|
||||
if (!errorThrown) {
|
||||
const errorContainer = panel.getByTestId("Panel:ErrorContainer");
|
||||
await expect(errorContainer).toBeVisible();
|
||||
await expect(errorContainer).toHaveText(new RegExp(expectedErrorMessage, "i"));
|
||||
}
|
||||
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
// Test successful job creation with valid job name
|
||||
const validJobName = expectedJobName;
|
||||
|
||||
const copyJobCreationPromise = waitForApiResponse(
|
||||
page,
|
||||
`${expectedAccountName}/dataTransferJobs/${validJobName}`,
|
||||
"PUT",
|
||||
);
|
||||
|
||||
await jobNameInput.fill(validJobName);
|
||||
await expect(copyButton).not.toHaveClass(/(^|\s)is-disabled(\s|$)/);
|
||||
|
||||
await copyButton.click();
|
||||
|
||||
const response = await copyJobCreationPromise;
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
// Verify panel closes and job appears in the list
|
||||
await expect(panel).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||
await jobsListContainer.waitFor({ state: "visible", timeout: 5000 });
|
||||
|
||||
const jobItem = jobsListContainer.getByText(validJobName);
|
||||
await jobItem.waitFor({ state: "visible", timeout: 5000 });
|
||||
await expect(jobItem).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,185 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { expect, Frame, Locator, Page, test } from "@playwright/test";
|
||||
import {
|
||||
ContainerCopy,
|
||||
getAccountName,
|
||||
getDropdownItemByNameOrPosition,
|
||||
TestAccount,
|
||||
waitForApiResponse,
|
||||
} from "../../fx";
|
||||
import { createMultipleTestContainers } from "../../testData";
|
||||
|
||||
test.describe("Container Copy - Online Migration", () => {
|
||||
let page: Page;
|
||||
let wrapper: Locator;
|
||||
let panel: Locator;
|
||||
let frame: Frame;
|
||||
let targetAccountName: string;
|
||||
|
||||
test.beforeEach("Setup for online migration test", async ({ browser }) => {
|
||||
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
|
||||
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
});
|
||||
|
||||
test.afterEach("Cleanup after online migration test", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test("Successfully create and manage online migration copy job", async () => {
|
||||
expect(wrapper).not.toBeNull();
|
||||
await wrapper.locator(".commandBarContainer").waitFor({ state: "visible" });
|
||||
|
||||
// Open Create Copy Job panel
|
||||
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||
await expect(createCopyJobButton).toBeVisible();
|
||||
await createCopyJobButton.click();
|
||||
panel = frame.getByTestId("Panel:Create copy job");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
// Reduced wait time for better performance
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Enable online migration mode
|
||||
const migrationTypeContainer = panel.getByTestId("migration-type");
|
||||
const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i });
|
||||
await onlineCopyRadioButton.click({ force: true });
|
||||
|
||||
await expect(migrationTypeContainer.getByTestId("migration-type-description-online")).toBeVisible();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Verify permissions screen is shown for online migration
|
||||
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
|
||||
await expect(permissionScreen).toBeVisible();
|
||||
await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||
|
||||
// Skip permissions setup and proceed to container selection
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Configure source and target containers for online migration
|
||||
const sourceDatabaseDropdown = panel.getByTestId("source-databaseDropdown");
|
||||
await sourceDatabaseDropdown.click();
|
||||
const sourceDbDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Database" },
|
||||
);
|
||||
await sourceDbDropdownItem.click();
|
||||
|
||||
const sourceContainerDropdown = panel.getByTestId("source-containerDropdown");
|
||||
await sourceContainerDropdown.click();
|
||||
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Container" },
|
||||
);
|
||||
await sourceContainerDropdownItem.click();
|
||||
|
||||
const targetDatabaseDropdown = panel.getByTestId("target-databaseDropdown");
|
||||
await targetDatabaseDropdown.click();
|
||||
const targetDbDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 0 },
|
||||
{ ariaLabel: "Database" },
|
||||
);
|
||||
await targetDbDropdownItem.click();
|
||||
|
||||
const targetContainerDropdown = panel.getByTestId("target-containerDropdown");
|
||||
await targetContainerDropdown.click();
|
||||
const targetContainerDropdownItem = await getDropdownItemByNameOrPosition(
|
||||
frame,
|
||||
{ position: 1 },
|
||||
{ ariaLabel: "Container" },
|
||||
);
|
||||
await targetContainerDropdownItem.click();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Verify job preview and create the online migration job
|
||||
const previewContainer = panel.getByTestId("Panel:PreviewCopyJob");
|
||||
await expect(previewContainer.getByTestId("source-account-name")).toHaveText(targetAccountName);
|
||||
|
||||
const jobNameInput = previewContainer.getByTestId("job-name-textfield");
|
||||
const onlineMigrationJobName = await jobNameInput.inputValue();
|
||||
|
||||
const copyButton = panel.getByRole("button", { name: "Copy", exact: true });
|
||||
|
||||
const copyJobCreationPromise = waitForApiResponse(
|
||||
page,
|
||||
`${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}`,
|
||||
"PUT",
|
||||
);
|
||||
await copyButton.click();
|
||||
await page.waitForTimeout(1000); // Reduced wait time
|
||||
|
||||
const response = await copyJobCreationPromise;
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
// Verify panel closes and job appears in the list
|
||||
await expect(panel).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
const jobsListContainer = wrapper.locator(".CopyJobListContainer .ms-DetailsList-contentWrapper .ms-List-page");
|
||||
await jobsListContainer.waitFor({ state: "visible", timeout: 5000 });
|
||||
|
||||
let jobRow, statusCell, actionMenuButton;
|
||||
jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName });
|
||||
statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||
await jobRow.waitFor({ state: "visible", timeout: 5000 });
|
||||
|
||||
// Verify job status changes to queued state
|
||||
await expect(statusCell).toContainText(/running|queued|pending/i, { timeout: 5000 });
|
||||
|
||||
// Test job lifecycle management through action menu
|
||||
actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`);
|
||||
await actionMenuButton.click();
|
||||
|
||||
// Test pause functionality
|
||||
const pauseAction = frame.locator(".ms-ContextualMenu-list button:has-text('Pause')");
|
||||
await pauseAction.click();
|
||||
|
||||
const pauseResponse = await waitForApiResponse(
|
||||
page,
|
||||
`${targetAccountName}/dataTransferJobs/${onlineMigrationJobName}/pause`,
|
||||
"POST",
|
||||
);
|
||||
expect(pauseResponse.ok()).toBe(true);
|
||||
|
||||
// Verify job status changes to paused
|
||||
jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName });
|
||||
await jobRow.waitFor({ state: "visible", timeout: 5000 });
|
||||
statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||
await expect(statusCell).toContainText(/paused/i, { timeout: 5000 });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Test cancel job functionality
|
||||
actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`);
|
||||
await actionMenuButton.click();
|
||||
await frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')").click();
|
||||
|
||||
// Verify cancellation confirmation dialog
|
||||
await expect(frame.locator(".ms-Dialog-main")).toBeVisible({ timeout: 2000 });
|
||||
await expect(frame.locator(".ms-Dialog-main")).toContainText(onlineMigrationJobName);
|
||||
|
||||
const cancelDialogButton = frame.locator(".ms-Dialog-main").getByTestId("DialogButton:Cancel");
|
||||
await expect(cancelDialogButton).toBeVisible();
|
||||
await cancelDialogButton.click();
|
||||
await expect(frame.locator(".ms-Dialog-main")).not.toBeVisible();
|
||||
|
||||
actionMenuButton = wrapper.getByTestId(`CopyJobActionMenu/Button:${onlineMigrationJobName}`);
|
||||
await actionMenuButton.click();
|
||||
await frame.locator(".ms-ContextualMenu-list button:has-text('Cancel')").click();
|
||||
|
||||
const confirmDialogButton = frame.locator(".ms-Dialog-main").getByTestId("DialogButton:Confirm");
|
||||
await expect(confirmDialogButton).toBeVisible();
|
||||
await confirmDialogButton.click();
|
||||
|
||||
// Verify final job status is cancelled
|
||||
jobRow = jobsListContainer.locator(".ms-DetailsRow", { hasText: onlineMigrationJobName });
|
||||
statusCell = jobRow.locator("[data-automationid='DetailsRowCell'][data-automation-key='CopyJobStatus']");
|
||||
await expect(statusCell).toContainText(/cancelled/i, { timeout: 5000 });
|
||||
});
|
||||
});
|
||||
@@ -1,270 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { expect, Frame, Locator, Page, test } from "@playwright/test";
|
||||
import { set } from "lodash";
|
||||
import { ContainerCopy, getAccountName, TestAccount } from "../../fx";
|
||||
|
||||
const VISIBLE_TIMEOUT_MS = 30 * 1000;
|
||||
|
||||
test.describe("Container Copy - Permission Screen Verification", () => {
|
||||
let page: Page;
|
||||
let wrapper: Locator;
|
||||
let panel: Locator;
|
||||
let frame: Frame;
|
||||
let targetAccountName: string;
|
||||
let expectedSourceAccountName: string;
|
||||
|
||||
test.beforeEach("Setup for each test", async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
});
|
||||
|
||||
test.afterEach("Cleanup after each test", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test("Verify online container copy permissions panel functionality", async () => {
|
||||
expect(wrapper).not.toBeNull();
|
||||
|
||||
// Verify all command bar buttons are visible
|
||||
await wrapper.locator(".commandBarContainer").waitFor({ state: "visible", timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
const createCopyJobButton = wrapper.getByTestId("CommandBar/Button:Create Copy Job");
|
||||
await expect(createCopyJobButton).toBeVisible();
|
||||
await expect(wrapper.getByTestId("CommandBar/Button:Refresh")).toBeVisible();
|
||||
await expect(wrapper.getByTestId("CommandBar/Button:Feedback")).toBeVisible();
|
||||
|
||||
// Open the Create Copy Job panel
|
||||
await createCopyJobButton.click();
|
||||
panel = frame.getByTestId("Panel:Create copy job");
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(panel.getByRole("heading", { name: "Create copy job" })).toBeVisible();
|
||||
|
||||
// Select a different account for cross-account testing
|
||||
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||
await accountDropdown.click();
|
||||
|
||||
const dropdownItemsWrapper = frame.locator("div.ms-Dropdown-items");
|
||||
expect(await dropdownItemsWrapper.getAttribute("aria-label")).toEqual("Account");
|
||||
|
||||
const allDropdownItems = await dropdownItemsWrapper.locator(`button.ms-Dropdown-item[role='option']`).all();
|
||||
|
||||
const filteredItems = [];
|
||||
for (const item of allDropdownItems) {
|
||||
const testContent = (await item.textContent()) ?? "";
|
||||
if (testContent.trim() !== targetAccountName.trim()) {
|
||||
filteredItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
const firstDropdownItem = filteredItems[0];
|
||||
expectedSourceAccountName = (await firstDropdownItem.textContent()) ?? "";
|
||||
await firstDropdownItem.click();
|
||||
} else {
|
||||
throw new Error("No dropdown items available after filtering");
|
||||
}
|
||||
|
||||
// Enable online migration mode
|
||||
const migrationTypeContainer = panel.getByTestId("migration-type");
|
||||
const onlineCopyRadioButton = migrationTypeContainer.getByRole("radio", { name: /Online mode/i });
|
||||
await onlineCopyRadioButton.click({ force: true });
|
||||
await expect(migrationTypeContainer.getByTestId("migration-type-description-online")).toBeVisible();
|
||||
|
||||
await panel.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Verify Assign Permissions panel for online copy
|
||||
const permissionScreen = panel.getByTestId("Panel:AssignPermissionsContainer");
|
||||
await expect(permissionScreen).toBeVisible();
|
||||
await expect(permissionScreen.getByText("Online container copy", { exact: true })).toBeVisible();
|
||||
await expect(permissionScreen.getByText("Cross-account container copy", { exact: true })).toBeVisible();
|
||||
|
||||
// Setup API mocking for the source account
|
||||
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}**`, async (route) => {
|
||||
const mockData = {
|
||||
identity: {
|
||||
type: "SystemAssigned",
|
||||
principalId: "00-11-22-33",
|
||||
},
|
||||
properties: {
|
||||
defaultIdentity: "SystemAssignedIdentity",
|
||||
backupPolicy: {
|
||||
type: "Continuous",
|
||||
},
|
||||
capabilities: [{ name: "EnableOnlineContainerCopy" }],
|
||||
},
|
||||
};
|
||||
if (route.request().method() === "GET") {
|
||||
const response = await route.fetch();
|
||||
const actualData = await response.json();
|
||||
const mergedData = { ...actualData };
|
||||
|
||||
set(mergedData, "identity", mockData.identity);
|
||||
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
|
||||
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
|
||||
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(mergedData),
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Verify Point-in-Time Restore functionality
|
||||
const expandedOnlineAccordionHeader = permissionScreen
|
||||
.getByTestId("permission-group-container-onlineConfigs")
|
||||
.locator("button[aria-expanded='true']");
|
||||
await expect(expandedOnlineAccordionHeader).toBeVisible();
|
||||
|
||||
const accordionItem = expandedOnlineAccordionHeader
|
||||
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
|
||||
.first();
|
||||
|
||||
const accordionPanel = accordionItem
|
||||
.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']")
|
||||
.first();
|
||||
|
||||
// Install clock mock and test PITR functionality
|
||||
await page.clock.install({ time: new Date("2024-01-01T10:00:00Z") });
|
||||
|
||||
const pitrBtn = accordionPanel.getByTestId("pointInTimeRestore:PrimaryBtn");
|
||||
await expect(pitrBtn).toBeVisible();
|
||||
await pitrBtn.click();
|
||||
|
||||
// Verify new page opens with correct URL pattern
|
||||
page.context().on("page", async (newPage) => {
|
||||
const expectedUrlEndPattern = new RegExp(
|
||||
`/providers/Microsoft.(DocumentDB|DocumentDb)/databaseAccounts/${expectedSourceAccountName}/backupRestore`,
|
||||
);
|
||||
expect(newPage.url()).toMatch(expectedUrlEndPattern);
|
||||
await newPage.close();
|
||||
});
|
||||
|
||||
const loadingOverlay = frame.locator("[data-test='loading-overlay']");
|
||||
await expect(loadingOverlay).toBeVisible();
|
||||
|
||||
const refreshBtn = accordionPanel.getByTestId("pointInTimeRestore:RefreshBtn");
|
||||
await expect(refreshBtn).not.toBeVisible();
|
||||
|
||||
// Fast forward time by 11 minutes
|
||||
await page.clock.fastForward(11 * 60 * 1000);
|
||||
|
||||
await expect(refreshBtn).toBeVisible({ timeout: 5000 });
|
||||
await expect(pitrBtn).not.toBeVisible();
|
||||
|
||||
// Setup additional API mocks for role assignments and permissions
|
||||
await page.route(
|
||||
`**/Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/sqlRoleAssignments*`,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
value: [
|
||||
{
|
||||
principalId: "00-11-22-33",
|
||||
roleDefinitionId: `Microsoft.DocumentDB/databaseAccounts/${expectedSourceAccountName}/77-88-99`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await page.route("**/Microsoft.DocumentDB/databaseAccounts/*/77-88-99**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
value: [
|
||||
{
|
||||
name: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route(`**/Microsoft.DocumentDB/databaseAccounts/${targetAccountName}**`, async (route) => {
|
||||
const mockData = {
|
||||
identity: {
|
||||
type: "SystemAssigned",
|
||||
principalId: "00-11-22-33",
|
||||
},
|
||||
properties: {
|
||||
defaultIdentity: "SystemAssignedIdentity",
|
||||
backupPolicy: {
|
||||
type: "Continuous",
|
||||
},
|
||||
capabilities: [{ name: "EnableOnlineContainerCopy" }],
|
||||
},
|
||||
};
|
||||
|
||||
if (route.request().method() === "PATCH") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ status: "Succeeded" }),
|
||||
});
|
||||
} else if (route.request().method() === "GET") {
|
||||
const response = await route.fetch();
|
||||
const actualData = await response.json();
|
||||
const mergedData = { ...actualData };
|
||||
set(mergedData, "identity", mockData.identity);
|
||||
set(mergedData, "properties.defaultIdentity", mockData.properties.defaultIdentity);
|
||||
set(mergedData, "properties.backupPolicy", mockData.properties.backupPolicy);
|
||||
set(mergedData, "properties.capabilities", mockData.properties.capabilities);
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(mergedData),
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Verify cross-account permissions functionality
|
||||
const expandedCrossAccordionHeader = permissionScreen
|
||||
.getByTestId("permission-group-container-crossAccountConfigs")
|
||||
.locator("button[aria-expanded='true']");
|
||||
await expect(expandedCrossAccordionHeader).toBeVisible();
|
||||
|
||||
const crossAccordionItem = expandedCrossAccordionHeader
|
||||
.locator("xpath=ancestor::*[contains(@class, 'fui-AccordionItem') or contains(@data-test, 'accordion-item')]")
|
||||
.first();
|
||||
|
||||
const crossAccordionPanel = crossAccordionItem
|
||||
.locator("[role='tabpanel'], .fui-AccordionPanel, [data-test*='panel']")
|
||||
.first();
|
||||
|
||||
const toggleButton = crossAccordionPanel.getByTestId("btn-toggle");
|
||||
await expect(toggleButton).toBeVisible();
|
||||
await toggleButton.click();
|
||||
|
||||
// Verify popover functionality
|
||||
const popover = frame.locator("[data-test='popover-container']");
|
||||
await expect(popover).toBeVisible();
|
||||
|
||||
const yesButton = popover.getByRole("button", { name: /Yes/i });
|
||||
const noButton = popover.getByRole("button", { name: /No/i });
|
||||
await expect(yesButton).toBeVisible();
|
||||
await expect(noButton).toBeVisible();
|
||||
|
||||
await yesButton.click();
|
||||
|
||||
// Verify loading states
|
||||
await expect(loadingOverlay).toBeVisible();
|
||||
await expect(loadingOverlay).toBeHidden({ timeout: 10 * 1000 });
|
||||
await expect(popover).toBeHidden({ timeout: 10 * 1000 });
|
||||
|
||||
// Cancel the panel to clean up
|
||||
await panel.getByRole("button", { name: "Cancel" }).click();
|
||||
});
|
||||
});
|
||||
363
test/sql/copy-job-create.md
Normal file
363
test/sql/copy-job-create.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# Copy Job Creation Test Plan
|
||||
|
||||
## Application Overview
|
||||
|
||||
This test plan covers comprehensive testing of the Copy Job Creation flow in the Azure CosmosDB Portal. The feature allows users to create container copy jobs for migrating data between Cosmos DB containers with support for both online and offline migration modes. Testing focuses on UI validation, form submission, error handling, and end-to-end workflow verification.
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. Copy Job Creation - Happy Path
|
||||
|
||||
**Seed:** `test/sql/copyjob.seed.spec.ts`
|
||||
|
||||
#### 1.1. Create Offline Copy Job Successfully
|
||||
|
||||
**File:** `tests/copy-job-creation/create-offline-copy-job-success.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Wait for the copy job screen to load
|
||||
2. Click 'Create Copy Job' button with data-test='CommandBar/Button:Create Copy Job'
|
||||
3. Verify side panel opens with data-test='Panel:Create copy job'
|
||||
4. Verify panel header contains 'Create copy job' text
|
||||
5. Wait for subscription dropdown with data-test='subscription-dropdown' to populate
|
||||
6. Wait for account dropdown with data-test='account-dropdown' to populate
|
||||
7. Verify 'Offline mode' radio button is enabled and selected by default
|
||||
8. Verify offline mode description with data-test='migration-type-description-offline' is visible
|
||||
9. Click 'Next' button to proceed to container selection
|
||||
10. Verify SelectSourceAndTargetContainers panel with data-test='Panel:SelectSourceAndTargetContainers' is visible
|
||||
11. Select first option from source database dropdown with data-test='source-databaseDropdown'
|
||||
12. Select first option from source container dropdown with data-test='source-containerDropdown'
|
||||
13. Select first option from target database dropdown with data-test='target-databaseDropdown'
|
||||
14. Select first option from target container dropdown with data-test='target-containerDropdown'
|
||||
15. Verify error message appears at top of panel for same source and target selection
|
||||
16. Select second option from target container dropdown to resolve error
|
||||
17. Click 'Next' button to proceed to preview
|
||||
18. Verify preview page displays selected source subscription, account, database and container
|
||||
19. Enter valid job name in job name input field
|
||||
20. Click 'Submit' button to create copy job
|
||||
21. Wait for API response indicating successful job creation
|
||||
22. Verify job appears in jobs list with correct name
|
||||
23. Verify side panel is closed after successful submission
|
||||
|
||||
**Expected Results:**
|
||||
- Copy job screen loads successfully
|
||||
- Create Copy Job button is visible and clickable
|
||||
- Side panel opens with correct title
|
||||
- Panel header displays 'Create copy job'
|
||||
- Subscription dropdown populates with available subscriptions
|
||||
- Account dropdown populates with available accounts
|
||||
- Offline mode radio button is enabled and selected
|
||||
- Offline mode description is visible and contains expected text
|
||||
- Navigation to container selection screen is successful
|
||||
- Source and target container selection panel is displayed
|
||||
- Database and container dropdowns populate with available options
|
||||
- Validation error is displayed for identical source and target containers
|
||||
- Error is resolved when different target container is selected
|
||||
- Navigation to preview screen is successful
|
||||
- Preview displays accurate selection details
|
||||
- Job name can be entered without restrictions
|
||||
- Copy job is successfully created via API
|
||||
- New job appears in the monitoring list
|
||||
- Side panel closes indicating completed workflow
|
||||
|
||||
#### 1.2. Create Online Copy Job Successfully
|
||||
|
||||
**File:** `tests/copy-job-creation/create-online-copy-job-success.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Wait for the copy job screen to load
|
||||
2. Click 'Create Copy Job' button
|
||||
3. Verify side panel opens
|
||||
4. Select 'Online mode' radio button
|
||||
5. Verify online mode description with data-test='migration-type-description-online' is visible
|
||||
6. Click 'Next' button
|
||||
7. Verify permissions assignment panel with data-test='Panel:AssignPermissionsContainer' is displayed
|
||||
8. Complete permissions assignment process
|
||||
9. Navigate to container selection screen
|
||||
10. Select source database and container
|
||||
11. Select different target database and container
|
||||
12. Navigate to preview screen
|
||||
13. Verify all selected details are accurate
|
||||
14. Enter valid job name
|
||||
15. Submit copy job creation
|
||||
16. Verify successful job creation and list update
|
||||
|
||||
**Expected Results:**
|
||||
- Online mode selection triggers permissions workflow
|
||||
- Permissions assignment panel is displayed correctly
|
||||
- Online copy job is created successfully
|
||||
- Job appears in list with online mode indicator
|
||||
- All workflow steps complete without errors
|
||||
|
||||
### 2. Copy Job Creation - Validation & Error Handling
|
||||
|
||||
**Seed:** `test/sql/copyjob.seed.spec.ts`
|
||||
|
||||
#### 2.1. Validate Required Field Errors
|
||||
|
||||
**File:** `tests/copy-job-creation/required-field-validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Open Create Copy Job panel
|
||||
2. Attempt to click 'Next' without selecting subscription
|
||||
3. Verify validation error for subscription field
|
||||
4. Select subscription but leave account unselected
|
||||
5. Attempt to proceed and verify account validation error
|
||||
6. Select account and proceed to container selection
|
||||
7. Attempt to proceed without selecting source database
|
||||
8. Verify source database validation error
|
||||
9. Select source database but leave container unselected
|
||||
10. Verify source container validation error
|
||||
11. Complete source selection but leave target unselected
|
||||
12. Verify target selection validation errors
|
||||
13. Complete all selections but proceed to preview with empty job name
|
||||
14. Verify job name validation error with data-test='Panel:ErrorContainer'
|
||||
|
||||
**Expected Results:**
|
||||
- Subscription selection is enforced
|
||||
- Account selection is required after subscription selection
|
||||
- Source database selection is mandatory
|
||||
- Source container selection is mandatory
|
||||
- Target database and container selection is mandatory
|
||||
- Job name is required and properly validated
|
||||
- All validation messages are clear and actionable
|
||||
|
||||
#### 2.2. Validate Job Name Input Restrictions
|
||||
|
||||
**File:** `tests/copy-job-creation/job-name-validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate through copy job creation to preview screen
|
||||
2. Test job name with special characters (!@#$%^&*())
|
||||
3. Verify validation error for invalid characters
|
||||
4. Test job name with spaces at beginning and end
|
||||
5. Verify whitespace handling
|
||||
6. Test extremely long job name (>100 characters)
|
||||
7. Verify length validation
|
||||
8. Test job name with only numbers
|
||||
9. Test job name starting with hyphen or underscore
|
||||
10. Test valid job name with alphanumeric, hyphens, and underscores
|
||||
11. Verify successful submission with valid name
|
||||
|
||||
**Expected Results:**
|
||||
- Special characters trigger appropriate validation error
|
||||
- Whitespace is handled according to business rules
|
||||
- Length restrictions are enforced
|
||||
- Numeric-only names are handled correctly
|
||||
- Valid naming patterns are accepted
|
||||
- Job creation succeeds with properly formatted name
|
||||
|
||||
#### 2.3. Handle Same Source and Target Selection Error
|
||||
|
||||
**File:** `tests/copy-job-creation/same-source-target-validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Complete subscription and account selection
|
||||
2. Navigate to container selection screen
|
||||
3. Select same database for both source and target
|
||||
4. Select same container for both source and target
|
||||
5. Attempt to proceed to next screen
|
||||
6. Verify error message appears with data-test='Panel:ErrorContainer'
|
||||
7. Verify error message content indicates source and target cannot be identical
|
||||
8. Change target container to different option
|
||||
9. Verify error message disappears
|
||||
10. Verify 'Next' button becomes enabled
|
||||
11. Successfully proceed to preview screen
|
||||
|
||||
**Expected Results:**
|
||||
- Identical source and target selection triggers validation error
|
||||
- Error message clearly explains the restriction
|
||||
- Error is dynamically resolved when selection changes
|
||||
- Navigation is blocked while validation error persists
|
||||
- Navigation resumes once valid selections are made
|
||||
|
||||
#### 2.4. Handle Network and API Errors
|
||||
|
||||
**File:** `tests/copy-job-creation/api-error-handling.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Complete entire copy job creation flow
|
||||
2. Mock API failure for job creation request
|
||||
3. Submit copy job with valid data
|
||||
4. Verify error message appears in panel
|
||||
5. Verify panel remains open for retry
|
||||
6. Mock network timeout scenario
|
||||
7. Verify appropriate timeout error handling
|
||||
8. Mock authentication error response
|
||||
9. Verify auth error is handled gracefully
|
||||
10. Restore normal API behavior
|
||||
11. Verify successful submission after resolving API issues
|
||||
|
||||
**Expected Results:**
|
||||
- API errors are caught and displayed to user
|
||||
- Error messages are informative and actionable
|
||||
- Panel remains accessible for retry attempts
|
||||
- Different error types are handled appropriately
|
||||
- Recovery flow works after resolving issues
|
||||
|
||||
### 3. Copy Job Creation - Edge Cases & Boundary Testing
|
||||
|
||||
**Seed:** `test/sql/copyjob.seed.spec.ts`
|
||||
|
||||
#### 3.1. Handle Large Numbers of Containers
|
||||
|
||||
**File:** `tests/copy-job-creation/large-container-lists.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Set up test account with 50+ databases and containers
|
||||
2. Open copy job creation panel
|
||||
3. Navigate to container selection
|
||||
4. Test dropdown performance with large lists
|
||||
5. Verify search/filter functionality in dropdowns
|
||||
6. Verify scrolling behavior in dropdown menus
|
||||
7. Select containers from end of large lists
|
||||
8. Complete job creation with selections from large datasets
|
||||
|
||||
**Expected Results:**
|
||||
- Dropdowns handle large datasets without performance issues
|
||||
- Search/filter functionality works correctly
|
||||
- Scrolling in dropdowns is smooth and responsive
|
||||
- Selections from any position in large lists work correctly
|
||||
- Job creation completes successfully with large dataset selections
|
||||
|
||||
#### 3.2. Test Unicode and International Characters
|
||||
|
||||
**File:** `tests/copy-job-creation/unicode-character-handling.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to job name input in preview screen
|
||||
2. Test job name with Chinese characters (测试工作)
|
||||
3. Test job name with Arabic characters (اختبار)
|
||||
4. Test job name with emoji characters (📁💾)
|
||||
5. Test job name with accented characters (café résumé)
|
||||
6. Test mixed unicode and ASCII characters
|
||||
7. Verify display and storage of unicode job names
|
||||
8. Submit jobs with various unicode names
|
||||
9. Verify unicode names appear correctly in jobs list
|
||||
|
||||
**Expected Results:**
|
||||
- Unicode characters are handled correctly in input fields
|
||||
- Job names with international characters are accepted
|
||||
- Unicode names display correctly throughout UI
|
||||
- Jobs with unicode names are created successfully
|
||||
- Unicode job names appear correctly in monitoring list
|
||||
|
||||
#### 3.3. Test Browser Compatibility Features
|
||||
|
||||
**File:** `tests/copy-job-creation/browser-compatibility.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Test copy job creation with browser back/forward navigation
|
||||
2. Verify panel state preservation during navigation
|
||||
3. Test browser refresh during copy job creation
|
||||
4. Verify appropriate handling of page refresh
|
||||
5. Test browser zoom levels (50%, 150%, 200%)
|
||||
6. Verify UI scaling and usability at different zoom levels
|
||||
7. Test with browser developer tools open
|
||||
8. Verify functionality with reduced viewport size
|
||||
|
||||
**Expected Results:**
|
||||
- Browser navigation doesn't break the copy job flow
|
||||
- Panel state is preserved appropriately
|
||||
- Page refresh handling is graceful
|
||||
- UI remains functional at various zoom levels
|
||||
- Responsive design adapts to different viewport sizes
|
||||
|
||||
#### 3.4. Test Concurrent User Scenarios
|
||||
|
||||
**File:** `tests/copy-job-creation/concurrent-operations.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Start copy job creation process
|
||||
2. Simulate another user creating job with same name
|
||||
3. Attempt to submit copy job with conflicting name
|
||||
4. Verify appropriate conflict resolution
|
||||
5. Test creating multiple jobs in rapid succession
|
||||
6. Verify each job creation is handled independently
|
||||
7. Test opening multiple copy job creation panels
|
||||
8. Verify panel management and state isolation
|
||||
|
||||
**Expected Results:**
|
||||
- Name conflicts are detected and handled appropriately
|
||||
- Multiple simultaneous job creations are managed correctly
|
||||
- Each copy job creation maintains independent state
|
||||
- Concurrent operations don't interfere with each other
|
||||
|
||||
### 4. Copy Job Creation - UI/UX Validation
|
||||
|
||||
**Seed:** `test/sql/copyjob.seed.spec.ts`
|
||||
|
||||
#### 4.1. Verify Panel Navigation and Controls
|
||||
|
||||
**File:** `tests/copy-job-creation/panel-navigation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Open copy job creation panel
|
||||
2. Verify 'Previous' button is disabled on first screen
|
||||
3. Verify 'Next' button state changes based on validation
|
||||
4. Navigate forward through all screens
|
||||
5. Verify 'Previous' button enables navigation backward
|
||||
6. Verify breadcrumb or progress indication
|
||||
7. Test 'Cancel' button at each step
|
||||
8. Verify cancel confirmation dialog if applicable
|
||||
9. Test panel close button (X) functionality
|
||||
10. Verify unsaved changes warning if applicable
|
||||
|
||||
**Expected Results:**
|
||||
- Navigation buttons are enabled/disabled appropriately
|
||||
- Forward navigation respects validation requirements
|
||||
- Backward navigation preserves user data
|
||||
- Progress indication is clear and accurate
|
||||
- Cancel functionality works consistently
|
||||
- Panel close behavior is user-friendly
|
||||
- Unsaved changes are handled appropriately
|
||||
|
||||
#### 4.2. Verify Dropdown and Form Controls
|
||||
|
||||
**File:** `tests/copy-job-creation/form-controls-validation.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Test all dropdown controls for proper data-test attributes
|
||||
2. Verify dropdown placeholder text is informative
|
||||
3. Test dropdown option selection and deselection
|
||||
4. Verify dropdown filtering/search if available
|
||||
5. Test keyboard navigation in dropdowns (Tab, Enter, Escape)
|
||||
6. Verify dropdown accessibility attributes (aria-label, role)
|
||||
7. Test radio button selection and mutual exclusivity
|
||||
8. Verify radio button labels and descriptions
|
||||
9. Test input field focus and blur behavior
|
||||
10. Verify form validation timing (on blur vs on submit)
|
||||
|
||||
**Expected Results:**
|
||||
- All dropdowns have correct data-test attributes for automation
|
||||
- Placeholder text provides clear guidance
|
||||
- Option selection works reliably
|
||||
- Keyboard navigation follows accessibility standards
|
||||
- ARIA attributes support screen readers
|
||||
- Radio buttons enforce single selection
|
||||
- Form validation provides immediate feedback
|
||||
- Input fields behave predictably
|
||||
|
||||
#### 4.3. Verify Loading States and Feedback
|
||||
|
||||
**File:** `tests/copy-job-creation/loading-states.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. Open copy job creation panel
|
||||
2. Verify loading indicators while fetching subscriptions
|
||||
3. Verify loading states for account dropdown population
|
||||
4. Test loading behavior for database and container lists
|
||||
5. Verify loading indicator during job submission
|
||||
6. Test behavior when API calls take longer than expected
|
||||
7. Verify loading state accessibility (announcements)
|
||||
8. Test loading state cancellation if supported
|
||||
9. Verify error states replace loading states appropriately
|
||||
10. Test loading state recovery after temporary failures
|
||||
|
||||
**Expected Results:**
|
||||
- Loading indicators appear for all asynchronous operations
|
||||
- Loading states are visually clear and accessible
|
||||
- Long-running operations provide appropriate feedback
|
||||
- Loading states can be cancelled when appropriate
|
||||
- Error handling gracefully replaces loading states
|
||||
- Recovery from temporary failures is smooth
|
||||
275
test/sql/copy-job-creation.spec.ts
Normal file
275
test/sql/copy-job-creation.spec.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { expect, Frame, Locator, Page, test } from "@playwright/test";
|
||||
import {
|
||||
ContainerCopy,
|
||||
getAccountName,
|
||||
getDropdownItemByNameOrPosition,
|
||||
TestAccount,
|
||||
waitForApiResponse
|
||||
} from "../fx";
|
||||
import { createMultipleTestContainers } from "../testData";
|
||||
|
||||
let page: Page;
|
||||
let wrapper: Locator = null!;
|
||||
let panel: Locator = null!;
|
||||
let frame: Frame = null!;
|
||||
let targetAccountName: string = "";
|
||||
let expectedJobName: string = "";
|
||||
const VISIBLE_TIMEOUT_MS = 30 * 1000;
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.describe("Copy Job Creation - Happy Path (Scenario 1)", () => {
|
||||
test.beforeAll("Copy Job Creation - Before All", async ({ browser }) => {
|
||||
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 3 });
|
||||
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
expectedJobName = `test_job_${Date.now()}`;
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
});
|
||||
|
||||
test.afterAll("Copy Job Creation - After All", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test.afterEach("Copy Job Creation - After Each", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
});
|
||||
|
||||
test("1.1. Create Offline Copy Job Successfully", async () => {
|
||||
// Step 1: Wait for the copy job screen to load
|
||||
await expect(wrapper.getByTestId("CommandBar/Button:Create Copy Job")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Step 2: Click 'Create Copy Job' button
|
||||
await wrapper.getByTestId("CommandBar/Button:Create Copy Job").click();
|
||||
|
||||
// Step 3: Verify side panel opens
|
||||
panel = frame.getByTestId("Panel:Create copy job");
|
||||
await expect(panel).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Step 4: Verify panel header contains 'Create copy job' text
|
||||
await expect(panel.getByText("Create copy job")).toBeVisible();
|
||||
|
||||
// Step 5: Wait for subscription dropdown to populate
|
||||
const subscriptionDropdown = panel.getByTestId("subscription-dropdown");
|
||||
await expect(subscriptionDropdown).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
await subscriptionDropdown.waitFor({ state: "visible" });
|
||||
|
||||
// Step 6: Wait for account dropdown to populate
|
||||
const accountDropdown = panel.getByTestId("account-dropdown");
|
||||
await expect(accountDropdown).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
await accountDropdown.waitFor({ state: "visible" });
|
||||
|
||||
// Step 7: Verify 'Offline mode' radio button is enabled and selected by default
|
||||
const offlineModeRadio = panel.getByRole("radio", { name: /offline/i });
|
||||
await expect(offlineModeRadio).toBeVisible();
|
||||
await expect(offlineModeRadio).toBeEnabled();
|
||||
await expect(offlineModeRadio).toBeChecked();
|
||||
|
||||
// Step 8: Verify offline mode description is visible
|
||||
const offlineModeDescription = panel.getByTestId("migration-type-description-offline");
|
||||
await expect(offlineModeDescription).toBeVisible();
|
||||
|
||||
// Step 9: Click 'Next' button to proceed to container selection
|
||||
const nextButton = panel.getByRole("button", { name: /next/i });
|
||||
await nextButton.click();
|
||||
|
||||
// Step 10: Verify SelectSourceAndTargetContainers panel is visible
|
||||
const containerSelectionPanel = frame.getByTestId("Panel:SelectSourceAndTargetContainers");
|
||||
await expect(containerSelectionPanel).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Step 11: Select first option from source database dropdown
|
||||
const sourceDatabaseDropdown = containerSelectionPanel.getByTestId("source-databaseDropdown");
|
||||
await expect(sourceDatabaseDropdown).toBeVisible();
|
||||
await sourceDatabaseDropdown.click();
|
||||
const sourceDatabaseDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 });
|
||||
await sourceDatabaseDropdownItem.click();
|
||||
|
||||
// Step 12: Select first option from source container dropdown
|
||||
const sourceContainerDropdown = containerSelectionPanel.getByTestId("source-containerDropdown");
|
||||
await expect(sourceContainerDropdown).toBeVisible();
|
||||
await sourceContainerDropdown.click();
|
||||
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 });
|
||||
await sourceContainerDropdownItem.click();
|
||||
|
||||
// Step 13: Select first option from target database dropdown
|
||||
const targetDatabaseDropdown = containerSelectionPanel.getByTestId("target-databaseDropdown");
|
||||
await expect(targetDatabaseDropdown).toBeVisible();
|
||||
await targetDatabaseDropdown.click();
|
||||
const targetDatabaseDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 });
|
||||
await targetDatabaseDropdownItem.click();
|
||||
|
||||
// Step 14: Select first option from target container dropdown
|
||||
const targetContainerDropdown = containerSelectionPanel.getByTestId("target-containerDropdown");
|
||||
await expect(targetContainerDropdown).toBeVisible();
|
||||
await targetContainerDropdown.click();
|
||||
const targetContainerDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 });
|
||||
await targetContainerDropdownItem.click();
|
||||
|
||||
// Step 15: Verify error message appears for same source and target selection
|
||||
const errorContainer = containerSelectionPanel.getByTestId("Panel:ErrorContainer");
|
||||
await expect(errorContainer).toBeVisible({ timeout: 5000 });
|
||||
await expect(errorContainer).toContainText(/source.*target.*identical|same/i);
|
||||
|
||||
// Step 16: Select second option from target container dropdown to resolve error
|
||||
await targetContainerDropdown.click();
|
||||
const targetContainerDropdownItemSecond = await getDropdownItemByNameOrPosition(frame, { position: 1 });
|
||||
await targetContainerDropdownItemSecond.click();
|
||||
|
||||
// Verify error message disappears
|
||||
await expect(errorContainer).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Step 17: Click 'Next' button to proceed to preview
|
||||
const nextButtonContainer = containerSelectionPanel.getByRole("button", { name: /next/i });
|
||||
await expect(nextButtonContainer).toBeEnabled();
|
||||
await nextButtonContainer.click();
|
||||
|
||||
// Step 18: Verify preview page displays selected details
|
||||
const previewPanel = frame.locator("[data-testid*='preview'], [data-testid*='Preview']").first();
|
||||
await expect(previewPanel).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Verify preview displays source and target information
|
||||
await expect(previewPanel.getByText(/source/i)).toBeVisible();
|
||||
await expect(previewPanel.getByText(/target/i)).toBeVisible();
|
||||
await expect(previewPanel.getByText(targetAccountName)).toBeVisible();
|
||||
|
||||
// Step 19: Enter valid job name
|
||||
const jobNameInput = previewPanel.getByRole("textbox").or(previewPanel.getByTestId("job-name-input"));
|
||||
await expect(jobNameInput).toBeVisible();
|
||||
await jobNameInput.fill(expectedJobName);
|
||||
|
||||
// Step 20: Click 'Submit' button to create copy job
|
||||
const submitButton = previewPanel.getByRole("button", { name: /submit/i });
|
||||
await expect(submitButton).toBeEnabled();
|
||||
|
||||
// Set up API response interception
|
||||
const apiResponsePromise = waitForApiResponse(
|
||||
page,
|
||||
`${targetAccountName}/dataTransferJobs/${expectedJobName}`,
|
||||
"PUT"
|
||||
);
|
||||
await submitButton.click();
|
||||
|
||||
// Step 21: Wait for API response indicating successful job creation
|
||||
await apiResponsePromise;
|
||||
|
||||
// Step 22: Verify job appears in jobs list
|
||||
await expect(panel).not.toBeVisible({ timeout: 10000 }); // Panel should close
|
||||
await expect(wrapper.getByText(expectedJobName)).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Step 23: Verify side panel is closed after successful submission
|
||||
await expect(frame.getByTestId("Panel:Create copy job")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("1.2. Create Online Copy Job Successfully", async () => {
|
||||
// Generate unique job name for this test
|
||||
const onlineJobName = `online_job_${Date.now()}`;
|
||||
|
||||
// Step 1: Wait for the copy job screen to load
|
||||
await expect(wrapper.getByTestId("CommandBar/Button:Create Copy Job")).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Step 2: Click 'Create Copy Job' button
|
||||
await wrapper.getByTestId("CommandBar/Button:Create Copy Job").click();
|
||||
|
||||
// Step 3: Verify side panel opens
|
||||
panel = frame.getByTestId("Panel:Create copy job");
|
||||
await expect(panel).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Step 4: Select 'Online mode' radio button
|
||||
const onlineModeRadio = panel.getByRole("radio", { name: /online/i });
|
||||
await expect(onlineModeRadio).toBeVisible();
|
||||
await expect(onlineModeRadio).toBeEnabled();
|
||||
await onlineModeRadio.click();
|
||||
await expect(onlineModeRadio).toBeChecked();
|
||||
|
||||
// Step 5: Verify online mode description is visible
|
||||
const onlineModeDescription = panel.getByTestId("migration-type-description-online");
|
||||
await expect(onlineModeDescription).toBeVisible();
|
||||
await expect(onlineModeDescription).toContainText(/online/i);
|
||||
|
||||
// Step 6: Click 'Next' button
|
||||
const nextButton = panel.getByRole("button", { name: /next/i });
|
||||
await nextButton.click();
|
||||
|
||||
// Step 7: Verify permissions assignment panel is displayed
|
||||
const permissionsPanel = frame.getByTestId("Panel:AssignPermissionsContainer");
|
||||
await expect(permissionsPanel).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Step 8: Complete permissions assignment process
|
||||
// This step may involve clicking through permission assignment UI
|
||||
const permissionsNextButton = permissionsPanel.getByRole("button", { name: /next|continue|proceed/i });
|
||||
if (await permissionsNextButton.isVisible()) {
|
||||
await permissionsNextButton.click();
|
||||
}
|
||||
|
||||
// Step 9: Navigate to container selection screen
|
||||
const containerSelectionPanel = frame.getByTestId("Panel:SelectSourceAndTargetContainers");
|
||||
await expect(containerSelectionPanel).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Step 10: Select source database and container
|
||||
const sourceDatabaseDropdown = containerSelectionPanel.getByTestId("source-databaseDropdown");
|
||||
await sourceDatabaseDropdown.click();
|
||||
const sourceDatabaseDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 });
|
||||
await sourceDatabaseDropdownItem.click();
|
||||
|
||||
const sourceContainerDropdown = containerSelectionPanel.getByTestId("source-containerDropdown");
|
||||
await sourceContainerDropdown.click();
|
||||
const sourceContainerDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 });
|
||||
await sourceContainerDropdownItem.click();
|
||||
// Step 11: Select different target database and container
|
||||
const targetDatabaseDropdown = containerSelectionPanel.getByTestId("target-databaseDropdown");
|
||||
await targetDatabaseDropdown.click();
|
||||
const targetDatabaseDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 0 });
|
||||
await targetDatabaseDropdownItem.click();
|
||||
|
||||
const targetContainerDropdown = containerSelectionPanel.getByTestId("target-containerDropdown");
|
||||
await targetContainerDropdown.click();
|
||||
const targetContainerDropdownItem = await getDropdownItemByNameOrPosition(frame, { position: 1 });
|
||||
await targetContainerDropdownItem.click(); // Select different container
|
||||
|
||||
// Step 12: Navigate to preview screen
|
||||
const nextButtonContainer = containerSelectionPanel.getByRole("button", { name: /next/i });
|
||||
await expect(nextButtonContainer).toBeEnabled();
|
||||
await nextButtonContainer.click();
|
||||
|
||||
// Step 13: Verify all selected details are accurate
|
||||
const previewPanel = frame.locator("[data-testid*='preview'], [data-testid*='Preview']").first();
|
||||
await expect(previewPanel).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Verify online mode is indicated in preview
|
||||
await expect(previewPanel.getByText(/online/i)).toBeVisible();
|
||||
await expect(previewPanel.getByText(targetAccountName)).toBeVisible();
|
||||
|
||||
// Step 14: Enter valid job name
|
||||
const jobNameInput = previewPanel.getByRole("textbox").or(previewPanel.getByTestId("job-name-input"));
|
||||
await jobNameInput.fill(onlineJobName);
|
||||
|
||||
// Step 15: Submit copy job creation
|
||||
const submitButton = previewPanel.getByRole("button", { name: /submit/i });
|
||||
await expect(submitButton).toBeEnabled();
|
||||
|
||||
// Set up API response interception for online job creation
|
||||
const apiResponsePromise = waitForApiResponse(
|
||||
page,
|
||||
`${targetAccountName}/dataTransferJobs/${onlineJobName}`,
|
||||
"PUT"
|
||||
);
|
||||
await submitButton.click();
|
||||
|
||||
// Step 16: Verify successful job creation and list update
|
||||
await apiResponsePromise;
|
||||
|
||||
// Verify panel closes and job appears in list
|
||||
await expect(panel).not.toBeVisible({ timeout: 10000 });
|
||||
await expect(wrapper.getByText(onlineJobName)).toBeVisible({ timeout: VISIBLE_TIMEOUT_MS });
|
||||
|
||||
// Verify online mode indicator in job list (if applicable)
|
||||
const jobListItem = wrapper.locator(`[data-testid*="job-item"], tr, .job-row`).filter({ hasText: onlineJobName });
|
||||
await expect(jobListItem).toBeVisible();
|
||||
|
||||
// Verify side panel is closed
|
||||
await expect(frame.getByTestId("Panel:Create copy job")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
32
test/sql/copyjob.seed.spec.ts
Normal file
32
test/sql/copyjob.seed.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Frame, Locator, Page, test } from '@playwright/test';
|
||||
import { ContainerCopy, getAccountName, TestAccount } from '../fx';
|
||||
import { createMultipleTestContainers } from '../testData';
|
||||
|
||||
test.describe('Copy Job Seed', () => {
|
||||
let page: Page;
|
||||
let wrapper: Locator = null!;
|
||||
let panel: Locator = null!;
|
||||
let frame: Frame = null!;
|
||||
let targetAccountName: string = "";
|
||||
test.beforeAll("Copy Job - Before All", async ({ browser }) => {
|
||||
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 3 });
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
targetAccountName = getAccountName(TestAccount.SQLContainerCopyOnly);
|
||||
});
|
||||
test.afterAll("Copy Job - After All", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
await page.close();
|
||||
});
|
||||
test.afterEach("Copy Job - After Each", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
});
|
||||
|
||||
test('Successfully create a copy job for offline migration', async ({ page }) => {
|
||||
// generate code here.
|
||||
});
|
||||
|
||||
test('Successfully create a copy job for online migration', async ({ page }) => {
|
||||
// generate code here.
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,12 @@ const { AzureCliCredential } = require("@azure/identity");
|
||||
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
||||
const ms = require("ms");
|
||||
|
||||
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"];
|
||||
const resourceGroupName = "de-e2e-tests";
|
||||
// const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"];
|
||||
// const resourceGroupName = "de-e2e-tests";
|
||||
const subscriptionId = "074d02eb-4d74-486a-b299-b262264d1536";
|
||||
const resourceGroupName = "bchoudhury-e2e-testing";
|
||||
|
||||
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
|
||||
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 5).getTime();
|
||||
|
||||
function friendlyTime(date) {
|
||||
try {
|
||||
@@ -18,7 +20,7 @@ function friendlyTime(date) {
|
||||
async function main() {
|
||||
const credentials = new AzureCliCredential();
|
||||
const client = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const accounts = await client.databaseAccounts.list(resourceGroupName);
|
||||
const accounts = await client.databaseAccounts.listByResourceGroup(resourceGroupName);
|
||||
for (const account of accounts) {
|
||||
if (account.name.endsWith("-readonly")) {
|
||||
console.log(`SKIPPED: ${account.name}`);
|
||||
|
||||
Reference in New Issue
Block a user