From a222a9fa8a118c34b9399f95da7a5dc66d43ef9f Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Thu, 21 May 2026 13:07:16 -0700 Subject: [PATCH] chore: e2e tests for compound fluent assertions + clean barrel exports - Add 2 e2e FOL solver tests for fluent .and/.or through checkAll() using a mock page with distinct geometry positions (P0-1 regression) - Remove 4 individual adapter factories from barrel index: createReactAdapter, createVueAdapter, createStorybookAdapter, createCustomAdapter (use react()/vue()/storybook()/custom() instead) - Reduce barrel from 101 to 89 lines --- packages/imhotep-playwright/src/index.ts | 12 --- .../imhotep-playwright/src/public.test.ts | 102 ++++++++++++++++++ 2 files changed, 102 insertions(+), 12 deletions(-) diff --git a/packages/imhotep-playwright/src/index.ts b/packages/imhotep-playwright/src/index.ts index 8b35384..157a21c 100644 --- a/packages/imhotep-playwright/src/index.ts +++ b/packages/imhotep-playwright/src/index.ts @@ -78,18 +78,6 @@ export type { CustomRendererOptions, } from './renderers.js' -export { createReactAdapter } from './react-adapter.js' -export type { ReactAdapterOptions } from './react-adapter.js' - -export { createVueAdapter } from './vue-adapter.js' -export type { VueAdapterOptions } from './vue-adapter.js' - -export { createStorybookAdapter } from './storybook-adapter.js' -export type { StorybookAdapterOptions } from './storybook-adapter.js' - -export { createCustomAdapter } from './custom-renderer-adapter.js' -export type { CustomAdapterOptions } from './custom-renderer-adapter.js' - // Reusable assertion presets. export { touchTarget, diff --git a/packages/imhotep-playwright/src/public.test.ts b/packages/imhotep-playwright/src/public.test.ts index 967520f..0b4890b 100644 --- a/packages/imhotep-playwright/src/public.test.ts +++ b/packages/imhotep-playwright/src/public.test.ts @@ -484,6 +484,108 @@ describe('FOL engine integration', () => { assert.ok(result.diagnostics.some((d) => d.code === 'IMH_EXTRACT_PROTOCOL_ERROR')) }) + it('compound .and chaining compiles through FOL solver and evaluates', async () => { + // Elements at distinct positions: + // .a: (10,10) — top-left + // .b: (110,10) — to the right of .a (gap: 50 >= 8) + // .c: (10,110) — below .a (gap: 50 >= 8) + const page: any = { + viewportSize: () => ({ width: 1280, height: 720 }), + setViewportSize: async () => {}, + emulateMedia: async () => {}, + addInitScript: async () => {}, + keyboard: { press: async () => {} }, + mouse: { move: async () => {} }, + context: () => ({}), + locator: (_selector: string) => ({ hover: async () => {}, focus: async () => {} }), + evaluate: async (_fn: any, ...args: any[]) => { + const payload = args[0] + if (!payload || !Array.isArray(payload.plans)) return undefined + const elements: Array = [] + const selectorToIds: Array<[string, number[]]> = [] + const positions: Record = { + '.a': { x: 10, y: 10 }, + '.b': { x: 110, y: 10 }, + '.c': { x: 10, y: 110 }, + } + for (const plan of payload.plans as Array<{ key: string; queries: string[] }>) { + const ids: number[] = [] + const pos = positions[plan.key] || { x: 0, y: 0, width: 0, height: 0 } + for (const _query of plan.queries) { + const subjectId = elements.length + 1 + elements.push({ + tagName: 'div', + rect: { x: pos.x, y: pos.y, width: 50, height: 50 }, + transform: { matrix: [1, 0, 0, 1, 0, 0], originX: 0, originY: 0 }, + }) + ids.push(subjectId) + } + selectorToIds.push([plan.key, ids]) + } + return { elements, selectorToIds } + }, + } + const ui = await imhotep(page) + + ui.expect('.a').to.be.leftOf('.b', { minGap: 8 }).and.above('.c', { minGap: 8 }) + + const result = await ui.checkAll() + assert.strictEqual(result.passed, true, `Expected passed:true, got passed:${result.passed}. Diagnostics: ${JSON.stringify(result.diagnostics.map(d => d.code))}`) + assert.strictEqual(result.clauseResults.length, 1) + assert.strictEqual(result.clauseResults[0].status, 'pass') + assert.ok(['true', 'determinate'].includes(result.clauseResults[0].truth), `expected true|determinate truth, got ${result.clauseResults[0].truth}`) + }) + + it('compound .or chaining compiles through FOL solver and evaluates', async () => { + // .a is leftOf .b (true) but NOT leftOf .c (false — same x column). + // With .or, true clause satisfies the disjunction. + const page: any = { + viewportSize: () => ({ width: 1280, height: 720 }), + setViewportSize: async () => {}, + emulateMedia: async () => {}, + addInitScript: async () => {}, + keyboard: { press: async () => {} }, + mouse: { move: async () => {} }, + context: () => ({}), + locator: (_selector: string) => ({ hover: async () => {}, focus: async () => {} }), + evaluate: async (_fn: any, ...args: any[]) => { + const payload = args[0] + if (!payload || !Array.isArray(payload.plans)) return undefined + const elements: Array = [] + const selectorToIds: Array<[string, number[]]> = [] + const positions: Record = { + '.a': { x: 10, y: 10 }, + '.b': { x: 110, y: 10 }, + '.c': { x: 10, y: 110 }, + } + for (const plan of payload.plans as Array<{ key: string; queries: string[] }>) { + const ids: number[] = [] + const pos = positions[plan.key] || { x: 0, y: 0, width: 0, height: 0 } + for (const _query of plan.queries) { + const subjectId = elements.length + 1 + elements.push({ + tagName: 'div', + rect: { x: pos.x, y: pos.y, width: 50, height: 50 }, + transform: { matrix: [1, 0, 0, 1, 0, 0], originX: 0, originY: 0 }, + }) + ids.push(subjectId) + } + selectorToIds.push([plan.key, ids]) + } + return { elements, selectorToIds } + }, + } + const ui = await imhotep(page) + + ui.expect('.a').to.be.leftOf('.b', { minGap: 8 }).or.leftOf('.c', { minGap: 8 }) + + const result = await ui.checkAll() + assert.strictEqual(result.passed, true, `Expected passed:true. Diagnostics: ${JSON.stringify(result.diagnostics.map(d => d.code))}`) + assert.strictEqual(result.clauseResults.length, 1) + assert.strictEqual(result.clauseResults[0].status, 'pass') + assert.ok(['true', 'determinate'].includes(result.clauseResults[0].truth), `expected true|determinate truth, got ${result.clauseResults[0].truth}`) + }) + it('forall over elements evaluates all matches', async () => { const page = createMockPage() const ui = await imhotep(page)