Playwright测试Angular应用:保存按钮触发无效表单及page未定义错误
问题解决:Playwright测试Angular应用时保存按钮点击报错及表单无效问题
一、修复ReferenceError: page is not defined错误
检查代码可见一行注释代码:
// await page.getByRole('button').click({ force: true });
这里直接使用了未定义的page变量,而非类实例的this.page。若后续启用该行代码会触发ReferenceError。确保所有页面操作代码均使用this.page,避免裸用未绑定的page变量。
二、解决“无效表单”提示问题
Angular表单依赖变更检测和用户交互事件更新控件状态,直接调用fill或selectOption可能无法触发Angular的表单验证逻辑,导致表单判定字段未填写。以下是针对性修复方案:
1. 模拟真实输入触发表单变更
替换部分fill为type,或在填充后触发input/blur事件,确保Angular检测到控件值变化:
// 用type模拟真实输入,触发Angular变更检测 await this.page.type('(//input[@id="organizationName"])[1]', customerData.customerName); // 触发blur事件,触发字段验证 await this.page.dispatchEvent('(//input[@id="organizationName"])[1]', 'blur');
2. 下拉选择后触发change事件
Angular下拉控件依赖change事件更新表单状态,选择选项后手动触发:
await this.page.selectOption("(//select[@id='opucode dropdown-toggle'])[2]", customerData.customerType); await this.page.dispatchEvent("(//select[@id='opucode dropdown-toggle'])[2]", 'change');
3. 正确处理日期选择器
直接填充日期输入框可能无法触发Angular日期控件的验证逻辑,建议通过点击日期面板选择日期:
// 打开日期选择面板 await this.page.click('#ej2-datepicker_0 input'); // 选择对应日期(需匹配日期面板的data-date属性格式) await this.page.click(`.e-calendar td[data-date="${customerData.InductionDate}"]`); // 等待输入框值更新完成 await this.page.waitForFunction( (selector, date) => document.querySelector(selector).value === date, "#ej2-datepicker_0_input", customerData.InductionDate );
4. 移除硬编码等待,改用状态等待
替换waitForTimeout为等待表单验证完成或元素状态变化,提升稳定性:
// 等待表单验证通过(等待所有无效状态元素消失) await this.page.waitForSelector('.ng-invalid', { state: 'detached', timeout: 10000 });
5. 点击保存前检查表单有效性
通过执行页面内代码检查表单状态,确认无误后再点击保存:
const isFormValid = await this.page.evaluate(() => { // 替换为你的表单选择器 const form = document.querySelector('form'); return form?.checkValidity(); }); if (isFormValid) { await this.page.getByRole('button', { name: 'Save' }).first().click(); } else { this.logger.error('表单验证未通过'); // 打印所有无效字段便于调试 const invalidFields = await this.page.evaluate(() => { return Array.from(document.querySelectorAll('.ng-invalid')).map(el => el.id || el.name); }); this.logger.error(`无效字段:${invalidFields.join(', ')}`); }
修改后的完整代码示例
async fillCustomerForm(customerData) { this.logger.info('填充客户详情'); try { // 等待文件输入元素可见 await this.page.waitForSelector("#fileInput"); // 设置文件输入并等待上传状态 await this.page.setInputFiles("#fileInput", "C:\\Users\\sathishd\\Downloads\\EtihadAirways.jpg"); await this.page.waitForSelector("#fileInput[value]", { timeout: 5000 }); // 填写组织名称并触发验证 await this.page.type('(//input[@id="organizationName"])[1]', customerData.customerName); await this.page.dispatchEvent('(//input[@id="organizationName"])[1]', 'blur'); // 填写短名称 const shortNameInput = this.page.getByLabel('Short Name'); await shortNameInput.click(); await shortNameInput.fill('SPR'); await shortNameInput.dispatchEvent('blur'); // 填写客户ECode await this.page.type("(//input[@id='shortName'])[2]", customerData.customerECode); await this.page.dispatchEvent("(//input[@id='shortName'])[2]", 'blur'); // 选择客户类型并触发变更 await this.page.selectOption("(//select[@id='opucode dropdown-toggle'])[2]", customerData.customerType); await this.page.dispatchEvent("(//select[@id='opucode dropdown-toggle'])[2]", 'change'); // 填写ICAO和IATA await this.page.type("(//input[@type='text'])[5]", customerData.ICAO); await this.page.dispatchEvent("(//input[@type='text'])[5]", 'blur'); await this.page.type("(//input[@type='text'])[6]", customerData.IATA); await this.page.dispatchEvent("(//input[@type='text'])[6]", 'blur'); // 处理日期选择器 await this.page.click('#ej2-datepicker_0 input'); await this.page.click(`.e-calendar td[data-date="${customerData.InductionDate}"]`); await this.page.waitForFunction( (selector, date) => document.querySelector(selector).value === date, "#ej2-datepicker_0_input", customerData.InductionDate ); // 选择自定义分类并触发变更 await this.page.selectOption("(//select[@id='opucode dropdown-toggle'])[3]", customerData.userdefinedCategory); await this.page.dispatchEvent("(//select[@id='opucode dropdown-toggle'])[3]", 'change'); // 等待表单验证通过 await this.page.waitForSelector('.ng-invalid', { state: 'detached', timeout: 10000 }); try { await this.page.waitForSelector('button:has-text("Save")', { visible: true, timeout: 5000 }); await this.page.getByRole('button', { name: 'Save' }).first().click(); this.logger.info('保存按钮点击成功'); } catch (error) { this.logger.error(`点击保存按钮出错: ${error}`); try { const screenshotPath = 'C:/sathish/AiDocx_Automation_Plywrt02/screenshot/save_button_error.png'; await this.page.screenshot({ path: screenshotPath }); this.logger.info(`已捕获截图: ${screenshotPath}`); } catch (screenshotError) { this.logger.error(`捕获截图出错: ${screenshotError}`); } } } catch (error) { this.logger.error(`填充表单出错: ${error}`); } }
内容的提问来源于stack exchange,提问作者Sathish Sathish
相关产品推荐
相关产品推荐

