TestCafe实现流式PageModel API时Selector上下文报错如何解决
解决方案
问题根源
TestCafe的Selector默认依赖同步测试执行栈隐式绑定测试控制器t,你把Selector执行逻辑放到thenable的then回调里时,这段逻辑是异步微任务执行,已经脱离了原测试函数的同步栈,所以TestCafe找不到上下文触发报错。
方案1:初始化PageModel时一次性绑定全局测试上下文(推荐)
仅需在创建PageModel实例时传入一次当前测试的t,在基类统一完成所有Selector的上下文绑定,无额外样板代码:
import { Selector, TestController } from 'testcafe'; // 通用流式PageModel基类,所有业务Page继承即可 class FluentPageModel { protected t: TestController; private actionQueue: Array<() => Promise<any>> = []; // 预绑定上下文的Selector构造器,内部所有Selector都用这个实例创建 protected boundSelector: typeof Selector; constructor(t: TestController) { this.t = t; this.boundSelector = Selector.with({ boundTestRun: t }); } // 统一操作入队方法,所有流式API调用该方法返回this实现链式调用 protected enqueue(action: () => Promise<any>) { this.actionQueue.push(action); return this; } // 实现thenable接口,await时自动执行所有队列操作 then(resolve: (value?: any) => void, reject: (reason?: any) => void) { delete (this as any).then; (async () => { try { let result; for (const action of this.actionQueue) { result = await action(); } // 重置状态支持后续二次链式调用 this.actionQueue = []; (this as any).then = this.then.bind(this); resolve(result); } catch (err) { this.actionQueue = []; (this as any).then = this.then.bind(this); reject(err); } })(); } } // 业务Page实现 class MyPage extends FluentPageModel { private focusElement = this.boundSelector(':focus'); private editButton = this.boundSelector('#edit-btn'); private saveButton = this.boundSelector('#save-btn'); // 原有独立异步方法不受影响 async asyncTest() { return await this.focusElement.exists; } // 流式API方法 navigateTo(path: string) { return this.enqueue(() => this.t.navigateTo(path)); } edit() { return this.enqueue(() => this.t.click(this.editButton)); } setField(fieldName: string, value: any) { return this.enqueue(() => this.t.typeText(this.boundSelector(`[name="${fieldName}"]`), value.toString())); } save() { return this.enqueue(() => this.t.click(this.saveButton)); } changeStatus(status: string) { return this.enqueue(() => this.t.click(this.boundSelector(`.status-${status}`))); } }
测试用例写法:
fixture `Demo` .page `https://google.com`; test('demo', async t => { const myPage = new MyPage(t); console.log('BEFORE') await myPage.asyncTest(); console.log('BETWEEN') await myPage.queuedTest(); // 不再触发上下文报错 console.log('AFTER') // 完全支持你需要的流式调用写法 await myPage .navigateTo('/form') .edit() .setField('amount', 12.34) .save() .changeStatus('complete'); });
方案2:自动捕获上下文无侵入写法
如果连初始化传t都不想写,可以利用TestCafe同步执行阶段隐式获取测试控制器的能力,不需要手动传参:
import { Selector, t } from 'testcafe'; class AutoBindFluentPage { private actionQueue: Array<() => Promise<any>> = []; // 同步执行阶段自动捕获当前测试的t private capturedT = t; private boundSelector = Selector.with({ boundTestRun: this.capturedT }); // 其余enqueue、then方法实现和方案1完全一致 // ... }
该方案直接const myPage = new AutoBindFluentPage()即可使用,仅需注意不要在测试内的异步回调中创建Page实例,否则会捕获不到上下文。
两种方案都完全避免了给每个Selector单独加with({boundTestRun: t})的样板代码,可维护性高。
内容的提问来源于stack exchange,提问作者Paul
相关产品推荐
相关产品推荐

