mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-01 13:33:48 +08:00
Fix : make playwright tests idempotent (#13332)
### What problem does this PR solve? Playwright tests previously depended on cross-file execution order (`auth -> provider -> dataset -> chat`). This change makes setup explicit and idempotent via fixtures so tests can run independently. - Added/standardized prerequisite fixtures in `test/playwright/conftest.py`: - `ensure_auth_context`, `ensure_model_provider_configured`, `ensure_dataset_ready`, `ensure_chat_ready` - Made provisioning reusable/idempotent with `RUN_ID`-based resource naming. - Synced auth envs (`E2E_ADMIN_EMAIL`, `E2E_ADMIN_PASSWORD`) into seeded creds. - Fixed provider cache freshness (`auth_header`/`page` refresh on cache hit). Also included minimal stability fixes: - dataset create stale-element click handling, - search wait logic for results/empty-state, - agent create-menu handling, - agent run-step retry when run UI doesn’t open first click. ### Type of change - [x] Test fix - [x] Refactoring --------- Co-authored-by: Liu An <asiro@qq.com>
This commit is contained in:
@@ -7,9 +7,8 @@ from urllib.parse import urljoin
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from test.playwright.helpers._auth_helpers import ensure_authed
|
||||
from test.playwright.helpers.flow_steps import flow_params, require
|
||||
from test.playwright.helpers.auth_selectors import EMAIL_INPUT, PASSWORD_INPUT, SUBMIT_BUTTON
|
||||
from test.playwright.helpers.auth_waits import wait_for_login_complete
|
||||
from test.playwright.helpers.response_capture import capture_response
|
||||
from test.playwright.helpers.datasets import (
|
||||
delete_uploaded_file,
|
||||
@@ -37,8 +36,6 @@ def step_01_login(
|
||||
auth_click,
|
||||
seeded_user_credentials,
|
||||
):
|
||||
email, password = seeded_user_credentials
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
file_paths = [
|
||||
repo_root / "test/benchmark/test_docs/Doc1.pdf",
|
||||
@@ -52,25 +49,14 @@ def step_01_login(
|
||||
flow_state["filenames"] = [path.name for path in file_paths]
|
||||
|
||||
with step("open login page"):
|
||||
flow_page.goto(login_url, wait_until="domcontentloaded")
|
||||
|
||||
form, _ = active_auth_context()
|
||||
email_input = form.locator(EMAIL_INPUT)
|
||||
password_input = form.locator(PASSWORD_INPUT)
|
||||
with step("fill credentials"):
|
||||
expect(email_input).to_have_count(1)
|
||||
expect(password_input).to_have_count(1)
|
||||
email_input.fill(email)
|
||||
password_input.fill(password)
|
||||
password_input.blur()
|
||||
|
||||
with step("submit login"):
|
||||
submit_button = form.locator(SUBMIT_BUTTON)
|
||||
expect(submit_button).to_have_count(1)
|
||||
auth_click(submit_button, "submit_login")
|
||||
|
||||
with step("wait for login"):
|
||||
wait_for_login_complete(flow_page, timeout_ms=RESULT_TIMEOUT_MS)
|
||||
ensure_authed(
|
||||
flow_page,
|
||||
login_url,
|
||||
active_auth_context,
|
||||
auth_click,
|
||||
seeded_user_credentials=seeded_user_credentials,
|
||||
timeout_ms=RESULT_TIMEOUT_MS,
|
||||
)
|
||||
flow_state["logged_in"] = True
|
||||
snap("login_complete")
|
||||
|
||||
@@ -276,6 +262,7 @@ def test_dataset_upload_parse_and_delete_flow(
|
||||
flow_state,
|
||||
base_url,
|
||||
login_url,
|
||||
ensure_model_provider_configured,
|
||||
active_auth_context,
|
||||
step,
|
||||
snap,
|
||||
|
||||
@@ -269,25 +269,31 @@ def step_06_run_agent(
|
||||
else:
|
||||
run_button = run_root
|
||||
expect(run_button).to_be_visible(timeout=RESULT_TIMEOUT_MS)
|
||||
try:
|
||||
auth_click(run_button, "agent_run")
|
||||
except Exception:
|
||||
page.wait_for_timeout(500)
|
||||
auth_click(run_button, "agent_run_retry")
|
||||
run_attempts = max(1, int(os.getenv("PW_AGENT_RUN_ATTEMPTS", "2")))
|
||||
last_error = None
|
||||
for attempt in range(run_attempts):
|
||||
if attempt > 0:
|
||||
page.wait_for_timeout(500)
|
||||
try:
|
||||
auth_click(run_button, f"agent_run_attempt_{attempt + 1}")
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
continue
|
||||
try:
|
||||
run_ui_locator.first.wait_for(state="visible", timeout=run_ui_timeout_ms)
|
||||
flow_state["agent_running"] = True
|
||||
snap("agent_run_started")
|
||||
return
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
|
||||
try:
|
||||
run_ui_locator.first.wait_for(state="visible", timeout=run_ui_timeout_ms)
|
||||
except Exception:
|
||||
_raise_with_diagnostics(
|
||||
page,
|
||||
"Agent run UI did not open after clicking Run.",
|
||||
snap=snap,
|
||||
snap_name="agent_run_missing",
|
||||
)
|
||||
|
||||
flow_state["agent_running"] = True
|
||||
snap("agent_run_started")
|
||||
return
|
||||
suffix = f" last_error={last_error}" if last_error else ""
|
||||
_raise_with_diagnostics(
|
||||
page,
|
||||
f"Agent run UI did not open after clicking Run ({run_attempts} attempts).{suffix}",
|
||||
snap=snap,
|
||||
snap_name="agent_run_missing",
|
||||
)
|
||||
|
||||
|
||||
def step_07_send_chat(
|
||||
@@ -378,6 +384,7 @@ def test_agent_create_then_import_json_then_run_and_wait_idle_flow(
|
||||
flow_state,
|
||||
base_url,
|
||||
login_url,
|
||||
ensure_dataset_ready,
|
||||
active_auth_context,
|
||||
step,
|
||||
snap,
|
||||
|
||||
@@ -108,6 +108,7 @@ def test_chat_create_select_dataset_and_receive_answer_flow(
|
||||
flow_state,
|
||||
base_url,
|
||||
login_url,
|
||||
ensure_chat_ready,
|
||||
active_auth_context,
|
||||
step,
|
||||
snap,
|
||||
|
||||
@@ -9,6 +9,7 @@ from test.playwright.helpers._next_apps_helpers import (
|
||||
_goto_home,
|
||||
_nav_click,
|
||||
_open_create_from_list,
|
||||
_search_query_input,
|
||||
_select_first_dataset_and_save,
|
||||
_unique_name,
|
||||
_wait_for_url_or_testid,
|
||||
@@ -20,7 +21,9 @@ def _wait_for_results_navigation(page, timeout_ms: int = RESULT_TIMEOUT_MS) -> N
|
||||
() => {
|
||||
const top = document.querySelector("[data-testid='top-nav']");
|
||||
const navs = Array.from(document.querySelectorAll('[role="navigation"]'));
|
||||
return navs.some((nav) => !top || !top.contains(nav));
|
||||
if (navs.some((nav) => !top || !top.contains(nav))) return true;
|
||||
const body = (document.body && document.body.innerText || '').toLowerCase();
|
||||
return body.includes('no results found');
|
||||
}
|
||||
"""
|
||||
page.wait_for_function(wait_js, timeout=timeout_ms)
|
||||
@@ -38,7 +41,10 @@ def _wait_for_results_navigation(page, timeout_ms: int = RESULT_TIMEOUT_MS) -> N
|
||||
)
|
||||
navs = page.locator("[role='navigation']")
|
||||
target = navs.first if index < 0 else navs.nth(index)
|
||||
expect(target).to_be_visible(timeout=timeout_ms)
|
||||
if index >= 0:
|
||||
expect(target).to_be_visible(timeout=timeout_ms)
|
||||
return
|
||||
expect(page.locator("text=/no results found/i").first).to_be_visible(timeout=timeout_ms)
|
||||
|
||||
|
||||
def step_01_ensure_authed(
|
||||
@@ -144,9 +150,7 @@ def step_05_select_dataset(
|
||||
require(flow_state, "search_created")
|
||||
page = flow_page
|
||||
with step("select dataset"):
|
||||
search_input = page.locator(
|
||||
"input[placeholder*='How can I help you today']"
|
||||
).first
|
||||
search_input = _search_query_input(page)
|
||||
_select_first_dataset_and_save(
|
||||
page,
|
||||
timeout_ms=RESULT_TIMEOUT_MS,
|
||||
@@ -169,7 +173,7 @@ def step_06_run_query(
|
||||
):
|
||||
require(flow_state, "search_input_ready")
|
||||
page = flow_page
|
||||
search_input = page.locator("input[placeholder*='How can I help you today']").first
|
||||
search_input = _search_query_input(page)
|
||||
with step("run search query"):
|
||||
expect(search_input).to_be_visible(timeout=RESULT_TIMEOUT_MS)
|
||||
search_input.fill("ragflow")
|
||||
@@ -197,6 +201,7 @@ def test_search_create_select_dataset_and_results_nav_appears_flow(
|
||||
flow_state,
|
||||
base_url,
|
||||
login_url,
|
||||
ensure_dataset_ready,
|
||||
active_auth_context,
|
||||
step,
|
||||
snap,
|
||||
|
||||
Reference in New Issue
Block a user