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')
|
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 () => {
|
it('supports quantifier entry via ui.expect.all(subject)', async () => {
|
||||||
const page = createMockPage()
|
const page = createMockPage()
|
||||||
const ui = await imhotep(page)
|
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 =>
|
const wrapBeProxy = (obj: any): any =>
|
||||||
new Proxy(obj, {
|
new Proxy(obj, {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
@@ -305,6 +328,7 @@ export async function imhotep(
|
|||||||
currentInStore = result
|
currentInStore = result
|
||||||
}
|
}
|
||||||
assertionStore.set(ui, currentList)
|
assertionStore.set(ui, currentList)
|
||||||
|
ensureAndOrProxied(result)
|
||||||
if (result && typeof result === 'object' && typeof (result as any).relation !== 'string') {
|
if (result && typeof result === 'object' && typeof (result as any).relation !== 'string') {
|
||||||
return wrapBeProxy(result)
|
return wrapBeProxy(result)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user