Files
ragflow/test/playwright/e2e/test_next_apps_search.py
Idriss Sbaaoui 2f4ca38adf 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>
2026-03-04 10:07:14 +08:00

222 lines
5.7 KiB
Python

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._next_apps_helpers import (
RESULT_TIMEOUT_MS,
_fill_and_save_create_modal,
_goto_home,
_nav_click,
_open_create_from_list,
_search_query_input,
_select_first_dataset_and_save,
_unique_name,
_wait_for_url_or_testid,
)
def _wait_for_results_navigation(page, timeout_ms: int = RESULT_TIMEOUT_MS) -> None:
wait_js = """
() => {
const top = document.querySelector("[data-testid='top-nav']");
const navs = Array.from(document.querySelectorAll('[role="navigation"]'));
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)
index = page.evaluate(
"""
() => {
const top = document.querySelector("[data-testid='top-nav']");
const navs = Array.from(document.querySelectorAll('[role="navigation"]'));
for (let i = 0; i < navs.length; i += 1) {
if (!top || !top.contains(navs[i])) return i;
}
return -1;
}
"""
)
navs = page.locator("[role='navigation']")
target = navs.first if index < 0 else navs.nth(index)
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(
flow_page,
flow_state,
base_url,
login_url,
active_auth_context,
step,
snap,
auth_click,
seeded_user_credentials,
):
with step("ensure logged in"):
ensure_authed(
flow_page,
login_url,
active_auth_context,
auth_click,
seeded_user_credentials=seeded_user_credentials,
)
flow_state["logged_in"] = True
snap("authed")
def step_02_open_search_list(
flow_page,
flow_state,
base_url,
login_url,
active_auth_context,
step,
snap,
auth_click,
seeded_user_credentials,
):
require(flow_state, "logged_in")
page = flow_page
with step("open search list"):
_goto_home(page, base_url)
_nav_click(page, "nav-search")
expect(page.locator("[data-testid='search-list']")).to_be_visible(
timeout=RESULT_TIMEOUT_MS
)
snap("search_list_open")
def step_03_open_create_modal(
flow_page,
flow_state,
base_url,
login_url,
active_auth_context,
step,
snap,
auth_click,
seeded_user_credentials,
):
require(flow_state, "logged_in")
page = flow_page
with step("open create search modal"):
_open_create_from_list(page, "search-empty-create", "create-search")
flow_state["search_modal_open"] = True
snap("search_create_modal")
def step_04_create_search(
flow_page,
flow_state,
base_url,
login_url,
active_auth_context,
step,
snap,
auth_click,
seeded_user_credentials,
):
require(flow_state, "search_modal_open")
page = flow_page
search_name = _unique_name("qa-search")
flow_state["search_name"] = search_name
with step("create search app"):
_fill_and_save_create_modal(page, search_name)
_wait_for_url_or_testid(page, r"/next-search/", "search-detail")
expect(page.locator("[data-testid='search-detail']")).to_be_visible(
timeout=RESULT_TIMEOUT_MS
)
flow_state["search_created"] = True
snap("search_created")
def step_05_select_dataset(
flow_page,
flow_state,
base_url,
login_url,
active_auth_context,
step,
snap,
auth_click,
seeded_user_credentials,
):
require(flow_state, "search_created")
page = flow_page
with step("select dataset"):
search_input = _search_query_input(page)
_select_first_dataset_and_save(
page,
timeout_ms=RESULT_TIMEOUT_MS,
post_save_ready_locator=search_input,
)
flow_state["search_input_ready"] = True
snap("search_dataset_saved")
def step_06_run_query(
flow_page,
flow_state,
base_url,
login_url,
active_auth_context,
step,
snap,
auth_click,
seeded_user_credentials,
):
require(flow_state, "search_input_ready")
page = flow_page
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")
search_input.press("Enter")
_wait_for_results_navigation(page, timeout_ms=RESULT_TIMEOUT_MS)
snap("search_results_nav")
STEPS = [
("01_ensure_authed", step_01_ensure_authed),
("02_open_search_list", step_02_open_search_list),
("03_open_create_modal", step_03_open_create_modal),
("04_create_search", step_04_create_search),
("05_select_dataset", step_05_select_dataset),
("06_run_query", step_06_run_query),
]
@pytest.mark.p1
@pytest.mark.auth
@pytest.mark.parametrize("step_fn", flow_params(STEPS))
def test_search_create_select_dataset_and_results_nav_appears_flow(
step_fn,
flow_page,
flow_state,
base_url,
login_url,
ensure_dataset_ready,
active_auth_context,
step,
snap,
auth_click,
seeded_user_credentials,
):
step_fn(
flow_page,
flow_state,
base_url,
login_url,
active_auth_context,
step,
snap,
auth_click,
seeded_user_credentials,
)