|
|
@@ -31,12 +31,33 @@ import { DEFAULT_TIMEOUTS } from "./constants";
|
|
|
export async function selectRadixOption(page: Page, label: string, value: string): Promise<void> {
|
|
|
console.debug(`[selectRadixOption] 开始选择: label="${label}", value="${value}"`);
|
|
|
const trigger = await findTrigger(page, label, value);
|
|
|
- console.debug(`[selectRadixOption] 找到触发器,准备点击`);
|
|
|
+ console.debug(`[selectRadixOption] 找到触发器,准备检查元素类型`);
|
|
|
+
|
|
|
+ // 检测是否是原生 select 元素
|
|
|
+ const element = "elementHandle" in trigger ? await trigger.elementHandle() : trigger;
|
|
|
+ const tagName = element ? await element.evaluate(el => el.tagName.toLowerCase()) : "";
|
|
|
+ const isNativeSelect = tagName === "select";
|
|
|
+
|
|
|
+ console.debug(`[selectRadixOption] 元素类型: ${tagName}, 是否原生 select: ${isNativeSelect}`);
|
|
|
+
|
|
|
+ if (isNativeSelect) {
|
|
|
+ // 原生 select 元素,使用 selectOption API
|
|
|
+ console.debug(`[selectRadixOption] 使用原生 select 方法`);
|
|
|
+ await trigger.selectOption(value);
|
|
|
+ console.debug(`[selectRadixOption] 选择完成`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Radix UI Select,点击展开选项列表
|
|
|
+ console.debug(`[selectRadixOption] 使用 Radix UI Select 方法`);
|
|
|
await trigger.click();
|
|
|
- console.debug(`[selectRadixOption] 已点击触发器,等待 listbox`);
|
|
|
- await page.waitForSelector("[role=listbox]", { timeout: DEFAULT_TIMEOUTS.static, state: "visible" });
|
|
|
- console.debug(`[selectRadixOption] listbox 已出现`);
|
|
|
- const availableOptions = await page.locator("[role=option]").allTextContents();
|
|
|
+ console.debug(`[selectRadixOption] 已点击触发器,等待选项出现`);
|
|
|
+
|
|
|
+ // 等待选项出现(使用 getByRole 查询 accessibility tree)
|
|
|
+ await page.getByRole("option").first().waitFor({ state: "visible", timeout: 2000 });
|
|
|
+ console.debug(`[selectRadixOption] 选项已出现`);
|
|
|
+
|
|
|
+ const availableOptions = await page.getByRole("option").allTextContents();
|
|
|
console.debug(`[selectRadixOption] 可用选项:`, availableOptions);
|
|
|
await findAndClickOption(page, value, availableOptions);
|
|
|
console.debug(`[selectRadixOption] 选择完成`);
|
|
|
@@ -57,7 +78,7 @@ export async function selectRadixOption(page: Page, label: string, value: string
|
|
|
* @throws {E2ETestError} 当触发器未找到时
|
|
|
*/
|
|
|
async function findTrigger(page: Page, label: string, expectedValue: string) {
|
|
|
- const timeout = DEFAULT_TIMEOUTS.static;
|
|
|
+ const timeout = 2000; // 使用较短超时快速尝试多个策略
|
|
|
const options = { timeout, state: "visible" as const };
|
|
|
|
|
|
// 策略 1: data-testid
|
|
|
@@ -102,7 +123,7 @@ async function findTrigger(page: Page, label: string, expectedValue: string) {
|
|
|
const comboboxCount = await combobox.count();
|
|
|
console.debug(`选择器策略4: 找到 ${comboboxCount} 个相邻的 combobox`);
|
|
|
if (comboboxCount > 0) {
|
|
|
- await combobox.waitFor({ state: "visible", timeout: DEFAULT_TIMEOUTS.static });
|
|
|
+ await combobox.waitFor({ state: "visible", timeout: 2000 });
|
|
|
console.debug(`选择器策略4成功: 找到相邻 combobox "${label}"`);
|
|
|
return combobox;
|
|
|
}
|
|
|
@@ -124,7 +145,8 @@ async function findTrigger(page: Page, label: string, expectedValue: string) {
|
|
|
* 查找并点击 Radix UI Select 选项
|
|
|
*
|
|
|
* @description
|
|
|
- * 按优先级尝试 data-value 和精确文本匹配两种策略。
|
|
|
+ * 使用 Playwright 的 getByRole 方法定位选项,比 waitForSelector 更可靠。
|
|
|
+ * 按优先级尝试 data-value 和无障碍名称两种策略。
|
|
|
*
|
|
|
* @internal
|
|
|
*
|
|
|
@@ -138,27 +160,50 @@ async function findAndClickOption(
|
|
|
value: string,
|
|
|
availableOptions: string[]
|
|
|
) {
|
|
|
- const timeout = DEFAULT_TIMEOUTS.static;
|
|
|
- const options = { timeout, state: "visible" as const };
|
|
|
+ const timeout = 2000; // 使用较短超时快速尝试多个策略
|
|
|
|
|
|
- // 策略 1: data-value(精确匹配)
|
|
|
- const dataValueSelector = `[role="option"][data-value="${value}"]`;
|
|
|
+ // 策略 1: 使用 getByRole 查找 option(推荐 - 使用 accessibility tree)
|
|
|
try {
|
|
|
- const option = await page.waitForSelector(dataValueSelector, options);
|
|
|
+ console.debug(`选项选择器策略1: getByRole("option", { name: "${value}" })`);
|
|
|
+ const option = page.getByRole("option", { name: value, exact: true });
|
|
|
+ // 先等待元素附加到 DOM(不要求可见)
|
|
|
+ await option.waitFor({ state: "attached", timeout });
|
|
|
+ // 然后等待可见
|
|
|
+ await option.waitFor({ state: "visible", timeout: 2000 });
|
|
|
await option.click();
|
|
|
+ // 等待下拉框关闭(选项消失)
|
|
|
+ await page.waitForTimeout(200);
|
|
|
+ // 等待选项列表消失
|
|
|
+ try {
|
|
|
+ await page.getByRole("option").first().waitFor({ state: "hidden", timeout: 1000 });
|
|
|
+ } catch {
|
|
|
+ // 选项可能已经消失或没有选项列表,忽略错误
|
|
|
+ }
|
|
|
+ console.debug(`选项选择器策略1成功`);
|
|
|
return;
|
|
|
} catch (err) {
|
|
|
- console.debug(`选项选择器策略1失败: ${dataValueSelector}`, err);
|
|
|
+ console.debug(`选项选择器策略1失败:`, err);
|
|
|
}
|
|
|
|
|
|
- // 策略 2: 精确文本匹配(使用 :text-is 避免部分匹配)
|
|
|
- const textSelector = `[role="option"]:text-is("${value}")`;
|
|
|
+ // 策略 2: data-value 属性(精确匹配)
|
|
|
try {
|
|
|
- const option = await page.waitForSelector(textSelector, options);
|
|
|
+ console.debug(`选项选择器策略2: [role="option"][data-value="${value}"]`);
|
|
|
+ const option = await page.waitForSelector(`[role="option"][data-value="${value}"]`, {
|
|
|
+ timeout,
|
|
|
+ state: "visible"
|
|
|
+ });
|
|
|
await option.click();
|
|
|
+ // 等待下拉框关闭
|
|
|
+ await page.waitForTimeout(200);
|
|
|
+ try {
|
|
|
+ await page.getByRole("option").first().waitFor({ state: "hidden", timeout: 1000 });
|
|
|
+ } catch {
|
|
|
+ // 忽略错误
|
|
|
+ }
|
|
|
+ console.debug(`选项选择器策略2成功`);
|
|
|
return;
|
|
|
} catch (err) {
|
|
|
- console.debug(`选项选择器策略2失败: ${textSelector}`, err);
|
|
|
+ console.debug(`选项选择器策略2失败:`, err);
|
|
|
}
|
|
|
|
|
|
// 未找到选项
|
|
|
@@ -220,20 +265,38 @@ export async function selectRadixOptionAsync(
|
|
|
|
|
|
// 2. 查找触发器(复用静态 Select 的逻辑)
|
|
|
const trigger = await findTrigger(page, label, value);
|
|
|
- console.debug(`[selectRadixOptionAsync] 找到触发器,准备点击`);
|
|
|
+ console.debug(`[selectRadixOptionAsync] 找到触发器,准备检查元素类型`);
|
|
|
+
|
|
|
+ // 3. 检测是否是原生 select 元素
|
|
|
+ const element = "elementHandle" in trigger ? await trigger.elementHandle() : trigger;
|
|
|
+ const tagName = element ? await element.evaluate(el => el.tagName.toLowerCase()) : "";
|
|
|
+ const isNativeSelect = tagName === "select";
|
|
|
|
|
|
- // 3. 点击触发器展开选项列表
|
|
|
+ console.debug(`[selectRadixOptionAsync] 元素类型: ${tagName}, 是否原生 select: ${isNativeSelect}`);
|
|
|
+
|
|
|
+ if (isNativeSelect) {
|
|
|
+ // 原生 select 元素,使用 selectOption API
|
|
|
+ console.debug(`[selectRadixOptionAsync] 使用原生 select 方法`);
|
|
|
+ await trigger.selectOption(value);
|
|
|
+ console.debug(`[selectRadixOptionAsync] 选择完成`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 点击 Radix UI Select 触发器展开选项列表
|
|
|
+ console.debug(`[selectRadixOptionAsync] 使用 Radix UI Select 方法`);
|
|
|
await trigger.click();
|
|
|
- console.debug(`[selectRadixOptionAsync] 已点击触发器,等待 listbox`);
|
|
|
+ console.debug(`[selectRadixOptionAsync] 已点击触发器,等待选项出现`);
|
|
|
|
|
|
- // 4. 等待选项列表容器出现
|
|
|
- await page.waitForSelector('[role="listbox"]', {
|
|
|
- timeout: DEFAULT_TIMEOUTS.static,
|
|
|
- state: 'visible'
|
|
|
+ // 5. 等待选项出现(Radix UI Select v2 没有 listbox,直接等待 option)
|
|
|
+ // 选项在 Portal 中渲染,需要短暂等待
|
|
|
+ await page.waitForTimeout(100);
|
|
|
+ await page.getByRole("option").first().waitFor({
|
|
|
+ state: "visible",
|
|
|
+ timeout: 2000
|
|
|
});
|
|
|
- console.debug(`[selectRadixOptionAsync] listbox 已出现`);
|
|
|
+ console.debug(`[selectRadixOptionAsync] 选项已出现`);
|
|
|
|
|
|
- // 5. 等待网络空闲(处理大量数据加载)
|
|
|
+ // 6. 等待网络空闲(处理大量数据加载)
|
|
|
// 注意:网络空闲等待失败不会中断流程,因为某些场景下网络可能始终不空闲
|
|
|
if (config.waitForNetworkIdle) {
|
|
|
console.debug(`[selectRadixOptionAsync] 等待网络空闲 (timeout: ${config.timeout}ms)`);
|
|
|
@@ -245,7 +308,7 @@ export async function selectRadixOptionAsync(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 6. 等待选项出现并选择
|
|
|
+ // 7. 等待选项出现并选择
|
|
|
if (config.waitForOption) {
|
|
|
console.debug(`[selectRadixOptionAsync] 等待选项加载 (timeout: ${config.timeout}ms)`);
|
|
|
await waitForOptionAndSelect(page, value, config.timeout);
|
|
|
@@ -266,8 +329,8 @@ export async function selectRadixOptionAsync(
|
|
|
* 然后完成选择操作。
|
|
|
*
|
|
|
* 按优先级尝试两种选择器策略:
|
|
|
- * 1. data-value 属性(精确匹配)
|
|
|
- * 2. 精确文本匹配(`:text-is()`)
|
|
|
+ * 1. getByRole("option", { name: value }) - 使用 accessibility tree
|
|
|
+ * 2. data-value 属性(精确匹配)
|
|
|
*
|
|
|
* @internal
|
|
|
*
|
|
|
@@ -287,19 +350,27 @@ async function waitForOptionAndSelect(
|
|
|
// 等待选项出现(使用重试机制)
|
|
|
while (Date.now() - startTime < timeout) {
|
|
|
try {
|
|
|
- // 策略 1: data-value(精确匹配)
|
|
|
- const dataValueSelector = `[role="option"][data-value="${value}"]`;
|
|
|
- const option = await page.waitForSelector(dataValueSelector, {
|
|
|
- timeout: retryInterval,
|
|
|
- state: 'visible'
|
|
|
- });
|
|
|
-
|
|
|
- if (option) {
|
|
|
- await option.click();
|
|
|
- return; // 成功选择
|
|
|
+ // 策略 1: getByRole("option", { name: value }) - 更可靠
|
|
|
+ console.debug(`异步选项选择策略1: getByRole("option", { name: "${value}" })`);
|
|
|
+ const option = page.getByRole("option", { name: value, exact: true });
|
|
|
+
|
|
|
+ // 等待元素附加到 DOM
|
|
|
+ await option.waitFor({ state: "attached", timeout: retryInterval });
|
|
|
+ // 等待元素可见
|
|
|
+ await option.waitFor({ state: "visible", timeout: 500 });
|
|
|
+
|
|
|
+ await option.click();
|
|
|
+ // 等待下拉框关闭
|
|
|
+ await page.waitForTimeout(200);
|
|
|
+ try {
|
|
|
+ await page.getByRole("option").first().waitFor({ state: "hidden", timeout: 1000 });
|
|
|
+ } catch {
|
|
|
+ // 忽略错误
|
|
|
}
|
|
|
- } catch {
|
|
|
- // 选项还未出现,继续尝试下一个策略
|
|
|
+ console.debug(`异步选项选择策略1成功`);
|
|
|
+ return; // 成功选择
|
|
|
+ } catch (err) {
|
|
|
+ console.debug(`异步选项选择策略1失败:`, err);
|
|
|
}
|
|
|
|
|
|
// 等待一小段时间后重试
|
|
|
@@ -310,20 +381,25 @@ async function waitForOptionAndSelect(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 策略 2 失败后,尝试文本匹配策略(一次性尝试,不重试)
|
|
|
+ // 策略 2: 尝试 data-value 属性匹配(一次性尝试,不重试)
|
|
|
try {
|
|
|
- const textSelector = `[role="option"]:text-is("${value}")`;
|
|
|
- const option = await page.waitForSelector(textSelector, {
|
|
|
- timeout: 1000,
|
|
|
+ console.debug(`异步选项选择策略2: [role="option"][data-value="${value}"]`);
|
|
|
+ const option = await page.waitForSelector(`[role="option"][data-value="${value}"]`, {
|
|
|
+ timeout: 2000,
|
|
|
state: 'visible'
|
|
|
});
|
|
|
-
|
|
|
- if (option) {
|
|
|
- await option.click();
|
|
|
- return; // 成功选择
|
|
|
+ await option.click();
|
|
|
+ // 等待下拉框关闭
|
|
|
+ await page.waitForTimeout(200);
|
|
|
+ try {
|
|
|
+ await page.getByRole("option").first().waitFor({ state: "hidden", timeout: 1000 });
|
|
|
+ } catch {
|
|
|
+ // 忽略错误
|
|
|
}
|
|
|
+ console.debug(`异步选项选择策略2成功`);
|
|
|
+ return; // 成功选择
|
|
|
} catch {
|
|
|
- // 文本策略也失败,继续抛出错误
|
|
|
+ // data-value 策略也失败,继续抛出错误
|
|
|
}
|
|
|
|
|
|
// 超时:获取当前可用的选项用于错误提示
|