fix: compound .and/.or chaining silently ignored in fluent API
The proxy in buildExpectation() returned FluentRelation raw (not proxied), so .and/.or chaining happened outside the assertion store. The second relation was never stored — checkAll() only evaluated the first relation (false positive). Add ensureAndOrProxied() to override .and/.or on FluentRelation instances so compound builders flow through the assertion store proxy. Preserves instanceof FluentRelation checks (no JS Proxy on the instance, just property overrides). 3 regression tests cover .and, .or, and triple-chaining.
This commit is contained in:
@@ -163,6 +163,56 @@ describe('Playwright public API', () => {
|
||||
assert.strictEqual(chain.relation, 'leftOf')
|
||||
})
|
||||
|
||||
it('compound .and chaining creates compound assertion with both parts', async () => {
|
||||
const page = createMockPage()
|
||||
const ui = await imhotep(page)
|
||||
|
||||
const chain = ui.expect('.a')
|
||||
.to.be.leftOf('.b', { minGap: 8 }).and.above('.c', { maxGap: 16 })
|
||||
|
||||
assert.strictEqual(chain.relation, 'above')
|
||||
const parts = (chain as any)._compoundParts as Array<{ relation: string; referenceSelector: string; options: Record<string, unknown> }> | undefined
|
||||
assert.ok(parts, 'compound parts should be defined')
|
||||
assert.strictEqual(parts!.length, 2)
|
||||
assert.strictEqual(parts![0].relation, 'leftOf')
|
||||
assert.strictEqual(parts![0].referenceSelector, '.b')
|
||||
assert.strictEqual(parts![1].relation, 'above')
|
||||
assert.strictEqual(parts![1].referenceSelector, '.c')
|
||||
assert.strictEqual(parts![1].options.maxGap, 16)
|
||||
})
|
||||
|
||||
it('compound .or chaining creates compound assertion with both parts', async () => {
|
||||
const page = createMockPage()
|
||||
const ui = await imhotep(page)
|
||||
|
||||
const chain = ui.expect('.x')
|
||||
.to.be.inside('.y').or.centeredWithin('.z')
|
||||
|
||||
assert.strictEqual(chain.relation, 'centeredWithin')
|
||||
const parts = (chain as any)._compoundParts as Array<{ relation: string; referenceSelector: string }> | undefined
|
||||
assert.ok(parts, 'compound parts should be defined')
|
||||
assert.strictEqual(parts!.length, 2)
|
||||
assert.strictEqual(parts![0].relation, 'inside')
|
||||
assert.strictEqual(parts![1].relation, 'centeredWithin')
|
||||
})
|
||||
|
||||
it('triple .and chaining accumulates all parts', async () => {
|
||||
const page = createMockPage()
|
||||
const ui = await imhotep(page)
|
||||
|
||||
const chain = ui.expect('.a')
|
||||
.to.be.leftOf('.b').and.above('.c').and.inside('.d')
|
||||
|
||||
assert.strictEqual(chain.relation, 'inside')
|
||||
const parts = (chain as any)._compoundParts as Array<{ relation: string; referenceSelector: string }> | undefined
|
||||
assert.ok(parts, 'compound parts should be defined for triple chain')
|
||||
assert.strictEqual(parts!.length, 3)
|
||||
assert.strictEqual(parts![0].relation, 'leftOf')
|
||||
assert.strictEqual(parts![1].relation, 'above')
|
||||
assert.strictEqual(parts![2].relation, 'inside')
|
||||
assert.strictEqual(parts![2].referenceSelector, '.d')
|
||||
})
|
||||
|
||||
it('supports quantifier entry via ui.expect.all(subject)', async () => {
|
||||
const page = createMockPage()
|
||||
const ui = await imhotep(page)
|
||||
|
||||
@@ -285,6 +285,29 @@ export async function imhotep(
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure .and / .or on a FluentRelation result return proxy-wrapped
|
||||
// builders so compound assertions flow through the assertion store.
|
||||
function ensureAndOrProxied(result: any): void {
|
||||
if (!result || typeof result !== 'object') return
|
||||
const proto = Object.getPrototypeOf(result)
|
||||
const andDesc = Object.getOwnPropertyDescriptor(proto, 'and')
|
||||
const orDesc = Object.getOwnPropertyDescriptor(proto, 'or')
|
||||
if (andDesc?.get) {
|
||||
Object.defineProperty(result, 'and', {
|
||||
get() { return wrapBeProxy(andDesc.get!.call(result)) },
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
if (orDesc?.get) {
|
||||
Object.defineProperty(result, 'or', {
|
||||
get() { return wrapBeProxy(orDesc.get!.call(result)) },
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const wrapBeProxy = (obj: any): any =>
|
||||
new Proxy(obj, {
|
||||
get(target, prop) {
|
||||
@@ -305,6 +328,7 @@ export async function imhotep(
|
||||
currentInStore = result
|
||||
}
|
||||
assertionStore.set(ui, currentList)
|
||||
ensureAndOrProxied(result)
|
||||
if (result && typeof result === 'object' && typeof (result as any).relation !== 'string') {
|
||||
return wrapBeProxy(result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user