776 lines
31 KiB
Python
776 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
QwenPaw E2E tests - Skill Pool P0 cases
|
|
|
|
Functional coverage:
|
|
1. Skill pool page load
|
|
2. Skill pool list display
|
|
3. Builtin skill source list
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import pytest
|
|
from playwright.sync_api import Page, expect
|
|
|
|
from config.settings import config
|
|
from pages.skill_pool_page import SkillPoolPage
|
|
from utils.helpers import log_test_step, log_test_result
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
BASE_URL = config.server.base_url
|
|
|
|
def navigate_to_skill_pool(page: Page):
|
|
"""Navigate to the skill pool page."""
|
|
page.goto(f"{BASE_URL}/skill-pool", wait_until="domcontentloaded", timeout=60000)
|
|
# Explicitly wait for skill cards to render rather than only relying on a fixed timeout
|
|
try:
|
|
page.wait_for_selector('.qwenpaw-card', timeout=15000)
|
|
except Exception:
|
|
logger.warning("Timed out waiting for skill cards; page may have no data or be slow")
|
|
page.wait_for_timeout(1000)
|
|
|
|
# ============================================================================
|
|
# POOL-001: Skill pool page load
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p0
|
|
class TestSkillPoolPageLoad:
|
|
"""POOL-001: Skill pool page load"""
|
|
|
|
@pytest.mark.test_id("POOL-001")
|
|
def test_skill_pool_page_load(self, page: Page, request: pytest.FixtureRequest):
|
|
"""Verify skill pool page loads normally."""
|
|
test_name = request.node.name
|
|
|
|
try:
|
|
log_test_step("1. Navigate to skill pool page")
|
|
navigate_to_skill_pool(page)
|
|
|
|
log_test_step("2. Verify page loaded")
|
|
body = page.locator("body").first
|
|
assert body.is_visible(timeout=5000), "Page should load"
|
|
logger.info("Skill pool page loaded")
|
|
|
|
log_test_result(test_name, "PASS", "Skill pool page load validation passed")
|
|
except Exception as e:
|
|
log_test_result(test_name, "FAIL", str(e))
|
|
raise
|
|
|
|
# ============================================================================
|
|
# POOL-P1-001: Skill pool search/filter
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
@pytest.mark.skill_pool
|
|
class TestSkillPoolSearch:
|
|
"""
|
|
POOL-P1-001: Skill pool search/filter
|
|
|
|
Functional coverage:
|
|
1. Verify search input exists
|
|
2. Enter keyword to filter skill list
|
|
3. Clear search to restore full list
|
|
"""
|
|
|
|
@pytest.mark.test_id("POOL-P1-001")
|
|
def test_skill_pool_search(self, page: Page, request: pytest.FixtureRequest):
|
|
"""Test skill pool search/filter functionality."""
|
|
test_name = request.node.name
|
|
|
|
log_test_step("Navigate to skill pool page")
|
|
navigate_to_skill_pool(page)
|
|
|
|
log_test_step("Verify search input exists")
|
|
search_input = page.locator(
|
|
'input[placeholder*="筛选"], input[placeholder*="搜索"], '
|
|
'input[placeholder*="search"], input[placeholder*="Search"], '
|
|
'input[placeholder*="filter"], '
|
|
'.qwenpaw-select-selection-search-input, '
|
|
'.qwenpaw-input-search input'
|
|
).first
|
|
expect(search_input).to_be_visible(timeout=5000)
|
|
logger.info("Search input exists")
|
|
|
|
log_test_step("Record skill count before search")
|
|
# Wait for cards to finish loading before counting, to avoid async data not yet arriving
|
|
try:
|
|
page.wait_for_selector('.qwenpaw-card', timeout=10000)
|
|
page.wait_for_timeout(500)
|
|
except Exception:
|
|
logger.warning("Did not see skill cards, page may have no data")
|
|
skill_cards = page.locator('.qwenpaw-card').all()
|
|
initial_count = len(skill_cards)
|
|
logger.info(f"Skill count before search: {initial_count}")
|
|
if initial_count == 0:
|
|
logger.info("Skill pool has no data, skipping search filter assertion")
|
|
log_test_result(test_name, True, 0)
|
|
return
|
|
|
|
log_test_step("Enter search keyword")
|
|
# Search input is a qwenpaw-select component (readonly input); click parent container to trigger dropdown
|
|
is_readonly = search_input.get_attribute("readonly") is not None
|
|
if is_readonly:
|
|
select_container = page.locator('.qwenpaw-select').first
|
|
select_container.click()
|
|
page.wait_for_timeout(500)
|
|
page.keyboard.type("nonexistent_skill_xyz")
|
|
page.wait_for_timeout(1500)
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(500)
|
|
else:
|
|
search_input.fill("nonexistent_skill_xyz")
|
|
page.wait_for_timeout(1500)
|
|
|
|
filtered_cards = page.locator('.qwenpaw-card').all()
|
|
filtered_count = len(filtered_cards)
|
|
logger.info(f"Skill count after search: {filtered_count}")
|
|
assert filtered_count <= initial_count, \
|
|
f"Filtered count ({filtered_count}) should not exceed initial count ({initial_count})"
|
|
logger.info("Search filter is effective")
|
|
|
|
log_test_step("Clear search to restore list")
|
|
if is_readonly:
|
|
clear_btn = page.locator('.qwenpaw-select-clear').first
|
|
if clear_btn.count() > 0:
|
|
clear_btn.click()
|
|
else:
|
|
select_container = page.locator('.qwenpaw-select').first
|
|
select_container.click()
|
|
page.wait_for_timeout(300)
|
|
page.keyboard.press("Control+a")
|
|
page.keyboard.press("Backspace")
|
|
page.keyboard.press("Escape")
|
|
else:
|
|
search_input.clear()
|
|
page.wait_for_timeout(1500)
|
|
|
|
restored_cards = page.locator('.qwenpaw-card').all()
|
|
restored_count = len(restored_cards)
|
|
logger.info(f"Skill count after clearing search: {restored_count}")
|
|
logger.info("List restored after clearing search")
|
|
|
|
log_test_result(test_name, True, 0)
|
|
|
|
# ============================================================================
|
|
# POOL-P1-002: Install skill to agent (via broadcast)
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
@pytest.mark.skill_pool
|
|
class TestSkillPoolInstall:
|
|
"""
|
|
POOL-P1-002: Install skill to agent
|
|
|
|
Functional coverage:
|
|
1. Find broadcast button
|
|
2. Open broadcast Modal
|
|
3. Verify Modal has skill and workspace selections
|
|
"""
|
|
|
|
@pytest.mark.test_id("POOL-P1-002")
|
|
def test_skill_pool_install(self, page: Page, request: pytest.FixtureRequest):
|
|
"""Test installing a skill to an agent."""
|
|
test_name = request.node.name
|
|
|
|
log_test_step("Navigate to skill pool page")
|
|
navigate_to_skill_pool(page)
|
|
|
|
log_test_step("Find broadcast button")
|
|
broadcast_btn = page.locator(
|
|
'button:has-text("广播"), button:has-text("Broadcast"), '
|
|
'button:has(.anticon-send)'
|
|
).first
|
|
|
|
if broadcast_btn.count() == 0:
|
|
logger.info("Broadcast button not found, skipping test")
|
|
log_test_result(test_name, True, 0)
|
|
return
|
|
|
|
expect(broadcast_btn).to_be_visible(timeout=5000)
|
|
logger.info("Broadcast button exists")
|
|
|
|
log_test_step("Click broadcast button")
|
|
broadcast_btn.click()
|
|
page.wait_for_timeout(1500)
|
|
|
|
log_test_step("Verify broadcast Modal opens")
|
|
page.wait_for_timeout(500)
|
|
visible_modals = page.locator('.qwenpaw-modal:visible, .ant-modal:visible, [role="dialog"]:visible')
|
|
modal = visible_modals.last if visible_modals.count() > 0 else page.locator('.qwenpaw-modal, .ant-modal').last
|
|
expect(modal).to_be_visible(timeout=8000)
|
|
modal_content = modal.inner_text()
|
|
assert len(modal_content) > 10, "Broadcast Modal content is empty"
|
|
logger.info(f"Broadcast Modal opened, content length: {len(modal_content)}")
|
|
|
|
log_test_step("Verify Modal has selection area and interact")
|
|
# Actual UI uses custom pickerCard component instead of standard checkbox/select
|
|
picker_cards = modal.locator('[class*=pickerCard]').all()
|
|
checkboxes = modal.locator('.qwenpaw-checkbox, .ant-checkbox, .qwenpaw-checkbox-wrapper').all()
|
|
selects = modal.locator('.qwenpaw-select, .ant-select').all()
|
|
lists = modal.locator('.qwenpaw-list-item, .ant-list-item, tr').all()
|
|
total_interactive = len(picker_cards) + len(checkboxes) + len(selects) + len(lists)
|
|
assert total_interactive > 0, "Broadcast Modal should have selectable elements (pickerCard/checkbox/select/list item)"
|
|
logger.info(f"Modal contains {len(picker_cards)} pickerCards, {len(checkboxes)} checkboxes, {len(selects)} selects, {len(lists)} list items")
|
|
|
|
# If pickerCards exist, click the first to verify interactivity
|
|
if len(picker_cards) > 0:
|
|
first_card = picker_cards[0]
|
|
first_card.click()
|
|
page.wait_for_timeout(500)
|
|
logger.info("Clicked the first pickerCard")
|
|
elif len(checkboxes) > 0:
|
|
first_checkbox = checkboxes[0]
|
|
first_checkbox.click()
|
|
page.wait_for_timeout(500)
|
|
logger.info("Checked the first checkbox")
|
|
|
|
# Verify confirm button exists
|
|
confirm_btn = modal.locator(
|
|
'button:has-text("OK"), button:has-text("确定"), '
|
|
'button:has-text("Broadcast"), button:has-text("广播"), '
|
|
'button.qwenpaw-btn-primary'
|
|
).first
|
|
assert confirm_btn.count() > 0, "Confirm button should exist in broadcast Modal"
|
|
logger.info("Confirm button exists")
|
|
|
|
log_test_step("Close Modal")
|
|
close_btn = modal.locator('.qwenpaw-modal-close, button:has-text("Cancel"), button:has-text("取消")').first
|
|
if close_btn.count() > 0:
|
|
close_btn.click()
|
|
else:
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(1000)
|
|
|
|
log_test_result(test_name, True, 0)
|
|
|
|
# ============================================================================
|
|
# POOL-P1-003: Broadcast skill to multiple agents
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
@pytest.mark.skill_pool
|
|
class TestSkillPoolBroadcast:
|
|
"""
|
|
POOL-P1-003: Broadcast skill to multiple agents
|
|
|
|
Functional coverage:
|
|
1. Open broadcast Modal
|
|
2. Verify multiple workspaces can be selected
|
|
3. Verify confirm button exists
|
|
"""
|
|
|
|
@pytest.mark.test_id("POOL-P1-003")
|
|
def test_skill_pool_broadcast(self, page: Page, request: pytest.FixtureRequest):
|
|
"""Test broadcasting skill to multiple agents."""
|
|
test_name = request.node.name
|
|
|
|
log_test_step("Navigate to skill pool page")
|
|
navigate_to_skill_pool(page)
|
|
|
|
log_test_step("Find and click broadcast button")
|
|
broadcast_btn = page.locator(
|
|
'button:has-text("广播"), button:has-text("Broadcast"), '
|
|
'button:has(.anticon-send)'
|
|
).first
|
|
|
|
if broadcast_btn.count() == 0:
|
|
logger.info("Broadcast button not found, skipping test")
|
|
log_test_result(test_name, True, 0)
|
|
return
|
|
|
|
broadcast_btn.click()
|
|
page.wait_for_timeout(3000)
|
|
|
|
# Wait for Modal to appear and grab the reference
|
|
modal_locator = page.locator('.qwenpaw-modal:visible, .ant-modal:visible, [role="dialog"]:visible')
|
|
expect(modal_locator.first).to_be_visible(timeout=8000)
|
|
modal = modal_locator.last
|
|
|
|
log_test_step("Verify workspace selection area and tick items")
|
|
# Actual UI uses custom pickerCard components; Modal contains two pickerSections:
|
|
# section 0: "select skill pool items", section 1: "broadcast to workspaces"
|
|
# Grab all pickerCards directly from Modal
|
|
workspace_items = modal.locator('[class*=pickerCard]').all()
|
|
if len(workspace_items) == 0:
|
|
# Fallback: try standard component selectors
|
|
workspace_items = modal.locator(
|
|
'.qwenpaw-checkbox-wrapper, .ant-checkbox-wrapper, '
|
|
'.qwenpaw-list-item, .ant-list-item'
|
|
).all()
|
|
assert len(workspace_items) > 0, "Broadcast Modal should have workspace/selection items"
|
|
logger.info(f"Found {len(workspace_items)} workspace/selection items")
|
|
|
|
# Click the first workspace item
|
|
first_item = workspace_items[0]
|
|
first_item.click()
|
|
page.wait_for_timeout(500)
|
|
logger.info("Selected the first workspace")
|
|
|
|
# If multiple items, click the second to verify multi-select
|
|
if len(workspace_items) > 1:
|
|
second_item = workspace_items[1]
|
|
second_item.click()
|
|
page.wait_for_timeout(500)
|
|
logger.info("Selected the second workspace (multi-select verified)")
|
|
|
|
log_test_step("Verify confirm button is enabled")
|
|
confirm_btn = modal.locator(
|
|
'button:has-text("OK"), button:has-text("确定"), '
|
|
'button:has-text("Broadcast"), button:has-text("广播"), '
|
|
'button.qwenpaw-btn-primary'
|
|
).first
|
|
assert confirm_btn.count() > 0, "Confirm button not found in broadcast Modal"
|
|
assert confirm_btn.is_enabled(), "Confirm button should be enabled after selecting workspaces"
|
|
logger.info("Confirm button exists and is enabled")
|
|
|
|
log_test_step("Close Modal (do not broadcast)")
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(1000)
|
|
|
|
log_test_result(test_name, True, 0)
|
|
|
|
# ============================================================================
|
|
# POOL-P1-004: Batch delete skills
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
@pytest.mark.skill_pool
|
|
class TestSkillPoolBatchDelete:
|
|
"""
|
|
POOL-P1-004: Batch delete skills
|
|
|
|
Functional coverage:
|
|
1. Find batch operation button
|
|
2. Enter batch mode
|
|
3. Verify checkboxes appear
|
|
4. Exit batch mode
|
|
"""
|
|
|
|
@pytest.mark.test_id("POOL-P1-004")
|
|
def test_skill_pool_batch_delete(self, page: Page, request: pytest.FixtureRequest):
|
|
"""Test skill pool batch delete functionality."""
|
|
test_name = request.node.name
|
|
|
|
log_test_step("Navigate to skill pool page")
|
|
navigate_to_skill_pool(page)
|
|
|
|
log_test_step("Find batch operation button")
|
|
batch_btn = page.locator(
|
|
'button:has-text("批量"), button:has-text("Batch"), '
|
|
'button:has-text("Select"), button:has-text("选择")'
|
|
).first
|
|
|
|
if batch_btn.count() == 0:
|
|
logger.info("Batch operation button not found, skipping test")
|
|
log_test_result(test_name, True, 0)
|
|
return
|
|
|
|
expect(batch_btn).to_be_visible(timeout=5000)
|
|
logger.info("Batch operation button exists")
|
|
|
|
log_test_step("Enter batch mode")
|
|
batch_btn.click()
|
|
page.wait_for_timeout(1500)
|
|
|
|
log_test_step("Verify checkboxes appear and select one")
|
|
checkboxes = page.locator('.qwenpaw-checkbox, .ant-checkbox, .qwenpaw-checkbox-wrapper').all()
|
|
assert len(checkboxes) > 0, "Checkboxes should appear in batch mode"
|
|
logger.info(f"Found {len(checkboxes)} checkboxes in batch mode")
|
|
|
|
# Check the first checkbox
|
|
checkboxes[0].click()
|
|
page.wait_for_timeout(500)
|
|
logger.info("Checked the first skill")
|
|
|
|
# Verify delete button appears and is enabled
|
|
delete_btn = page.locator(
|
|
'button:has-text("删除"), button:has-text("Delete"), '
|
|
'button.qwenpaw-btn-dangerous'
|
|
).first
|
|
if delete_btn.count() > 0 and delete_btn.is_visible(timeout=3000):
|
|
assert delete_btn.is_enabled(), "Delete button should be enabled after selecting a skill"
|
|
logger.info("Delete button is visible and enabled")
|
|
else:
|
|
# Verify select-all button exists
|
|
select_all = page.locator(
|
|
'button:has-text("全选"), button:has-text("Select All"), '
|
|
'.qwenpaw-checkbox-wrapper:has-text("全选")'
|
|
).first
|
|
if select_all.count() > 0:
|
|
logger.info("Select-all button exists")
|
|
else:
|
|
logger.info("No delete/select-all button found")
|
|
|
|
log_test_step("Exit batch mode (do not delete)")
|
|
# Click batch button again or click cancel
|
|
cancel_btn = page.locator(
|
|
'button:has-text("取消"), button:has-text("Cancel"), '
|
|
'button:has-text("退出"), button:has-text("Exit")'
|
|
).first
|
|
if cancel_btn.count() > 0:
|
|
cancel_btn.click()
|
|
else:
|
|
batch_btn.click()
|
|
page.wait_for_timeout(1000)
|
|
logger.info("Exited batch mode")
|
|
|
|
log_test_result(test_name, True, 0)
|
|
|
|
# ============================================================================
|
|
# POOL-P1-005: ZIP import skill
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
@pytest.mark.skill_pool
|
|
class TestSkillPoolZipImport:
|
|
"""
|
|
POOL-P1-005: ZIP import skill
|
|
|
|
Functional coverage:
|
|
1. Find ZIP upload button
|
|
2. Verify hidden file input exists with accept attribute .zip
|
|
3. Create a temp zip file and upload via hidden input
|
|
4. Verify upload succeeded (skill appears in list or success message shown)
|
|
5. Cleanup: delete uploaded skill + delete temp file
|
|
"""
|
|
|
|
@pytest.mark.test_id("POOL-P1-005")
|
|
def test_skill_pool_zip_import(self, page: Page, request: pytest.FixtureRequest):
|
|
"""Test skill pool ZIP import (with actual upload)."""
|
|
import zipfile
|
|
import tempfile
|
|
import os
|
|
import time
|
|
|
|
test_name = request.node.name
|
|
skill_name = f"e2e_pool_zip_{int(time.time())}"
|
|
zip_path = None
|
|
skill_uploaded = False
|
|
|
|
try:
|
|
log_test_step("1. Navigate to skill pool page")
|
|
navigate_to_skill_pool(page)
|
|
|
|
log_test_step("2. Find ZIP upload button")
|
|
upload_btn = page.locator(
|
|
'button:has-text("zip"), button:has-text("ZIP"), '
|
|
'button:has-text("上传"), button:has-text("Upload"), '
|
|
'button:has(.anticon-upload)'
|
|
).first
|
|
|
|
if upload_btn.count() == 0:
|
|
pytest.skip("ZIP upload button not found, skipping test")
|
|
|
|
expect(upload_btn).to_be_visible(timeout=5000)
|
|
logger.info("ZIP upload button exists")
|
|
|
|
log_test_step("3. Verify hidden file input")
|
|
file_input = page.locator(
|
|
'input[type="file"][accept=".zip"], '
|
|
'input[type="file"][accept*="zip"]'
|
|
).first
|
|
assert file_input.count() > 0, "Hidden ZIP file input not found"
|
|
|
|
accept_attr = file_input.get_attribute("accept")
|
|
assert ".zip" in accept_attr, f"File input accept attribute does not include .zip: {accept_attr}"
|
|
logger.info(f"File input accept={accept_attr}")
|
|
|
|
log_test_step("4. Record initial skill count")
|
|
initial_cards = page.locator('.qwenpaw-card').all()
|
|
initial_count = len(initial_cards)
|
|
logger.info(f"Initial skill count: {initial_count}")
|
|
|
|
log_test_step("5. Create temporary zip file")
|
|
skill_content = f"""---
|
|
name: {skill_name}
|
|
description: E2E test skill uploaded via zip to skill pool
|
|
---
|
|
|
|
# {skill_name}
|
|
|
|
This is a test skill uploaded via zip for E2E testing.
|
|
"""
|
|
temp_dir = tempfile.mkdtemp()
|
|
md_path = os.path.join(temp_dir, f"{skill_name}.md")
|
|
zip_path = os.path.join(temp_dir, f"{skill_name}.zip")
|
|
|
|
with open(md_path, "w", encoding="utf-8") as md_file:
|
|
md_file.write(skill_content)
|
|
|
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
zf.write(md_path, f"{skill_name}.md")
|
|
|
|
logger.info(f"Temporary zip file created: {zip_path}")
|
|
|
|
log_test_step("6. Upload zip file via hidden input")
|
|
file_input.set_input_files(zip_path)
|
|
logger.info("Uploaded zip file via set_input_files")
|
|
|
|
# Wait for upload to finish processing
|
|
page.wait_for_timeout(5000)
|
|
|
|
log_test_step("7. Verify upload result")
|
|
# Check for success message
|
|
success_message = page.locator(
|
|
'.qwenpaw-message-success, '
|
|
'.qwenpaw-message-notice:has-text("成功"), '
|
|
'.qwenpaw-message-notice:has-text("success")'
|
|
).first
|
|
if success_message.is_visible():
|
|
logger.info("Detected upload success message")
|
|
|
|
# Refresh page to ensure list updates
|
|
page.reload()
|
|
page.wait_for_load_state("domcontentloaded")
|
|
page.wait_for_timeout(3000)
|
|
|
|
# Verify the new skill appears in the list
|
|
new_skill_locator = page.locator(f'text="{skill_name}"').first
|
|
try:
|
|
expect(new_skill_locator).to_be_visible(timeout=8000)
|
|
skill_uploaded = True
|
|
logger.info(f"Uploaded skill appeared in the skill pool list: {skill_name}")
|
|
except Exception:
|
|
updated_cards = page.locator('.qwenpaw-card').all()
|
|
updated_count = len(updated_cards)
|
|
logger.info(f"Skill count after upload: {updated_count} (initial: {initial_count})")
|
|
if updated_count > initial_count:
|
|
skill_uploaded = True
|
|
logger.info("Skill count increased, upload likely succeeded")
|
|
else:
|
|
logger.warning("No new skill detected; upload may have failed or name mismatch")
|
|
|
|
log_test_result(test_name, True, 0)
|
|
logger.info(f"Test {test_name} passed - skill pool ZIP import validation passed")
|
|
|
|
finally:
|
|
# Cleanup: delete the uploaded test skill
|
|
if skill_uploaded:
|
|
try:
|
|
target_card = page.locator(f'.qwenpaw-card:has-text("{skill_name}")').first
|
|
if target_card.is_visible():
|
|
# Try to find delete button on the card
|
|
target_card.hover()
|
|
page.wait_for_timeout(500)
|
|
delete_btn = target_card.locator(
|
|
'button.qwenpaw-btn-dangerous, '
|
|
'button:has-text("删除"), '
|
|
'button:has-text("Delete"), '
|
|
'button:has(.anticon-delete)'
|
|
).first
|
|
if delete_btn.is_visible():
|
|
delete_btn.click()
|
|
page.wait_for_timeout(1000)
|
|
confirm_btn = page.locator(
|
|
'.qwenpaw-modal-confirm-btns button.qwenpaw-btn-dangerous, '
|
|
'.qwenpaw-modal button.qwenpaw-btn-dangerous, '
|
|
'.qwenpaw-modal button.qwenpaw-btn-primary'
|
|
).first
|
|
if confirm_btn.is_visible():
|
|
confirm_btn.click()
|
|
page.wait_for_timeout(2000)
|
|
logger.info(f"Cleanup: deleted test skill '{skill_name}'")
|
|
except Exception:
|
|
logger.warning(f"Cleanup failed: could not delete test skill '{skill_name}'")
|
|
|
|
# Cleanup: delete temp file
|
|
if zip_path:
|
|
try:
|
|
import shutil
|
|
temp_dir_to_clean = os.path.dirname(zip_path)
|
|
shutil.rmtree(temp_dir_to_clean, ignore_errors=True)
|
|
logger.info("Cleanup: deleted temporary zip file")
|
|
except Exception:
|
|
logger.warning("Cleanup failed: could not delete temp file")
|
|
|
|
|
|
# ============================================================================
|
|
# POOL-P2-001: Import builtin skill pack
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p2
|
|
@pytest.mark.skill_pool
|
|
class TestSkillPoolBuiltinImport:
|
|
"""POOL-P2-001: Import builtin skill pack"""
|
|
|
|
@pytest.mark.test_id("POOL-P2-001")
|
|
def test_skill_pool_builtin_import(self, page: Page, request: pytest.FixtureRequest):
|
|
"""Test importing the builtin skill pack."""
|
|
test_name = request.node.name
|
|
|
|
log_test_step("Navigate to skill pool page")
|
|
navigate_to_skill_pool(page)
|
|
|
|
log_test_step("Find builtin skill import button")
|
|
builtin_btn = page.locator(
|
|
'button:has-text("内置"), button:has-text("Built"), '
|
|
'button:has-text("Builtin"), button:has-text("Update"), '
|
|
'button:has-text("更新")'
|
|
).first
|
|
|
|
if builtin_btn.count() == 0:
|
|
pytest.skip("Builtin skill import button not found, skipping test")
|
|
|
|
expect(builtin_btn).to_be_visible(timeout=5000)
|
|
logger.info("Builtin skill import button exists")
|
|
|
|
builtin_btn.click()
|
|
page.wait_for_timeout(2000)
|
|
|
|
# Check if a dialog/drawer opened, or import ran directly
|
|
modal_or_drawer = page.locator('.qwenpaw-modal, .ant-modal, .qwenpaw-drawer, .ant-drawer, [role="dialog"]').last
|
|
if modal_or_drawer.count() > 0:
|
|
try:
|
|
expect(modal_or_drawer).to_be_visible(timeout=5000)
|
|
logger.info("Builtin skill import dialog opened")
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(500)
|
|
except Exception:
|
|
logger.info("Dialog exists but not visible, may have auto-closed")
|
|
else:
|
|
# Possibly the click triggered import directly (no dialog)
|
|
success_msg = page.locator('.qwenpaw-message-success, .ant-message-success').first
|
|
if success_msg.count() > 0:
|
|
logger.info("Builtin skill import executed (no dialog confirmation)")
|
|
else:
|
|
logger.info("No dialog appeared after click, may be running in background")
|
|
|
|
log_test_result(test_name, True, 0)
|
|
|
|
|
|
# ============================================================================
|
|
# SYNC-001 P1 Skill card shows sync-status badge + auto-update button
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
@pytest.mark.skill_pool
|
|
@pytest.mark.skill_sync
|
|
class TestSkillAutoSyncCard:
|
|
"""SYNC-001: a pool skill card carries a sync-status badge and, on hover,
|
|
the auto-update quick-toggle button (upstream #5639)."""
|
|
|
|
SKILL_NAME = "e2e_sync_card_skill"
|
|
|
|
@pytest.mark.test_id("SYNC-001")
|
|
def test_skill_card_sync_badge_and_button(
|
|
self,
|
|
page: Page,
|
|
api_context,
|
|
request: pytest.FixtureRequest,
|
|
):
|
|
test_name = request.node.name
|
|
pool = SkillPoolPage(page)
|
|
try:
|
|
log_test_step("1. Seed a pool skill via API (no LLM)")
|
|
SkillPoolPage.delete_pool_skill(api_context, self.SKILL_NAME)
|
|
assert SkillPoolPage.seed_pool_skill(
|
|
api_context, self.SKILL_NAME
|
|
), "Failed to seed pool skill"
|
|
|
|
log_test_step("2. Open the Skill Pool page (card view is default)")
|
|
pool.open()
|
|
|
|
log_test_step("3. Locate the seeded skill card")
|
|
card = pool.find_card_by_name(self.SKILL_NAME)
|
|
assert card is not None, f"Seeded card not found: {self.SKILL_NAME}"
|
|
expect(card).to_be_visible(timeout=pool.timeout)
|
|
|
|
log_test_step("4. Card shows a sync-status badge with a colored dot")
|
|
expect(
|
|
card.locator(pool.STATUS_BADGE).first
|
|
).to_be_visible(timeout=pool.timeout)
|
|
expect(
|
|
card.locator(pool.STATUS_DOT).first
|
|
).to_be_visible(timeout=pool.timeout)
|
|
|
|
log_test_step("5. Hovering the card reveals the auto-update button")
|
|
pool.hover_card(card)
|
|
expect(
|
|
card.locator(pool.AUTO_UPDATE_BUTTON).first
|
|
).to_be_visible(timeout=pool.timeout)
|
|
|
|
log_test_result(test_name, True, 0)
|
|
logger.info(f"Test {test_name} passed")
|
|
finally:
|
|
SkillPoolPage.delete_pool_skill(api_context, self.SKILL_NAME)
|
|
|
|
|
|
# ============================================================================
|
|
# SYNC-002 P1 Edit-drawer Auto Sync switch reveals targets + persists on Save
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.p1
|
|
@pytest.mark.skill_pool
|
|
@pytest.mark.skill_sync
|
|
class TestSkillAutoSyncDrawer:
|
|
"""SYNC-002: in the edit drawer, turning the Auto Sync switch ON reveals the
|
|
target-agent select; clicking Save persists it (card shows the Auto Sync
|
|
tag afterwards). The switch is staged — nothing persists until Save."""
|
|
|
|
SKILL_NAME = "e2e_sync_drawer_skill"
|
|
|
|
@pytest.mark.test_id("SYNC-002")
|
|
def test_auto_sync_switch_reveals_targets_and_persists(
|
|
self,
|
|
page: Page,
|
|
api_context,
|
|
request: pytest.FixtureRequest,
|
|
):
|
|
test_name = request.node.name
|
|
pool = SkillPoolPage(page)
|
|
try:
|
|
log_test_step("1. Seed a pool skill (auto_update defaults off)")
|
|
SkillPoolPage.delete_pool_skill(api_context, self.SKILL_NAME)
|
|
assert SkillPoolPage.seed_pool_skill(
|
|
api_context, self.SKILL_NAME
|
|
), "Failed to seed pool skill"
|
|
|
|
log_test_step("2. Open the Skill Pool page and the skill's edit drawer")
|
|
pool.open()
|
|
pool.open_edit_drawer(self.SKILL_NAME)
|
|
|
|
log_test_step("3. Auto Sync switch visible; target select hidden")
|
|
expect(
|
|
page.locator(pool.AUTO_SYNC_SWITCH).first
|
|
).to_be_visible(timeout=pool.timeout)
|
|
target_before = page.locator(pool.TARGET_SELECT_PLACEHOLDER)
|
|
assert (
|
|
target_before.count() == 0
|
|
or not target_before.first.is_visible()
|
|
), "Target-agent select should be hidden while Auto Sync is off"
|
|
|
|
log_test_step("4. Turn Auto Sync ON → target-agent select appears")
|
|
pool.toggle_auto_sync_switch()
|
|
expect(
|
|
page.locator(pool.TARGET_SELECT_PLACEHOLDER).first
|
|
).to_be_visible(timeout=pool.timeout)
|
|
|
|
log_test_step("5. Save → the drawer closes")
|
|
pool.save_drawer()
|
|
expect(
|
|
page.locator(pool.DRAWER).first
|
|
).to_be_hidden(timeout=pool.timeout)
|
|
|
|
log_test_step("6. The card now shows the Auto Sync tag (persisted)")
|
|
card = pool.find_card_by_name(self.SKILL_NAME)
|
|
assert card is not None, "Card missing after save"
|
|
expect(
|
|
card.locator(pool.AUTO_UPDATE_TAG).first
|
|
).to_be_visible(timeout=pool.timeout)
|
|
|
|
log_test_result(test_name, True, 0)
|
|
logger.info(f"Test {test_name} passed")
|
|
finally:
|
|
SkillPoolPage.delete_pool_skill(api_context, self.SKILL_NAME)
|