1124 lines
39 KiB
TypeScript
1124 lines
39 KiB
TypeScript
|
|
import { test, expect } from '@playwright/test'
|
||
|
|
import { imhotep } from 'imhotep-playwright'
|
||
|
|
import { loadFixtureInPage, waitForFixtureReady } from './harness.js'
|
||
|
|
|
||
|
|
async function fixtureUrl(category: string): Promise<string> {
|
||
|
|
const path = await loadFixtureInPage({ goto: async () => {} }, category)
|
||
|
|
const { resolveFixturePage } = await import('./harness.js')
|
||
|
|
return 'file://' + resolveFixturePage(category)
|
||
|
|
}
|
||
|
|
|
||
|
|
test.describe('E2E: Public API — leftOf vertical slice', () => {
|
||
|
|
test('leftOf with minGap:8 passes when actual gap is 10px', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.leftOf('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults.length).toBe(1)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('leftOf with minGap:20 fails when actual gap is 10px', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.leftOf('[data-testid="box-right"]', { minGap: 20 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults.length).toBe(1)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('failing leftOf includes diagnostic with measured gap value', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.leftOf('[data-testid="box-right"]', { minGap: 20 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.diagnostics.length).toBeGreaterThan(0)
|
||
|
|
|
||
|
|
const diag = result.diagnostics.find((d: { code: string; message: string }) => d.code === 'IMH_RELATION_LEFT_OF_FAILED')
|
||
|
|
expect(diag).toBeDefined()
|
||
|
|
expect(diag!.message).toContain('measured gap is')
|
||
|
|
expect(diag!.message).toContain('10')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('assertion on non-existent selector fails with extraction error', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('.does-not-exist').to.be.leftOf('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('error')
|
||
|
|
expect(result.diagnostics.some((d: { code: string }) => d.code === 'IMH_SELECTOR_ZERO_MATCHES')).toBe(true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('valid assertions pass when other assertions have missing selectors', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.leftOf('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
ui.expect('.does-not-exist').to.be.leftOf('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults.length).toBe(2)
|
||
|
|
|
||
|
|
// The valid assertion should pass
|
||
|
|
const validCr = result.clauseResults.find((cr: any) => cr.clauseLabel?.includes('box-left'))
|
||
|
|
expect(validCr?.status).toBe('pass')
|
||
|
|
|
||
|
|
// The invalid assertion should fail with selector error
|
||
|
|
const invalidCr = result.clauseResults.find((cr: any) => cr.clauseLabel?.includes('does-not-exist'))
|
||
|
|
expect(invalidCr?.status).toBe('error')
|
||
|
|
expect(invalidCr?.diagnostics).toContain('IMH_SELECTOR_ZERO_MATCHES')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('separatedFrom passes when elements do not overlap', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
// box-left and box-right are in a flex container with 10px gap — they do not overlap
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.separatedFrom('[data-testid="box-right"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('above with minGap:8 passes when actual gap is 10px', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-above"]').to.be.above('[data-testid="box-below"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('above with minGap:20 fails when actual gap is 10px', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-above"]').to.be.above('[data-testid="box-below"]', { minGap: 20 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('below with minGap:8 passes when actual gap is 10px', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-below"]').to.be.below('[data-testid="box-above"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('below with minGap:20 fails when actual gap is 10px', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-below"]').to.be.below('[data-testid="box-above"]', { minGap: 20 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('inside passes when subject is fully within reference', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-inside"]').to.be.inside('[data-testid="container-inside"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('inside fails when subject overflows reference', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="container-inside"]').to.be.inside('[data-testid="box-inside"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('alignedWith centerY passes when centers align', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="align-subject"]').to.be.alignedWith('[data-testid="align-ref"]', { axis: 'centerY', tolerance: 0 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('alignedWith centerY fails when misaligned beyond tolerance', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-above"]').to.be.alignedWith('[data-testid="box-below"]', { axis: 'centerY', tolerance: 0 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("atLeast('44px').wide passes when width >= 44", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atLeast('44px').wide
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("atLeast('100px').wide fails when width < 100", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atLeast('100px').wide
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("atLeast('44px').tall passes when height >= 44", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atLeast('44px').tall
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("atLeast('100px').tall fails when height < 100", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atLeast('100px').tall
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('rightOf with minGap:8 passes when actual gap is 10px', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-right"]').to.be.rightOf('[data-testid="box-left"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults.length).toBe(1)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('rightOf with minGap:20 fails when actual gap is 10px', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-right"]').to.be.rightOf('[data-testid="box-left"]', { minGap: 20 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults.length).toBe(1)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('centeredWithin passes when element is centered', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-centered"]').to.be.centeredWithin('[data-testid="center-container"]', { tolerance: 0 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('centeredWithin fails when element is offset', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-offset"]').to.be.centeredWithin('[data-testid="center-container-offset"]', { tolerance: 0 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("atMost('100px').wide passes when width <= max", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atMost('100px').wide
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("atMost('50px').wide fails when width > max", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atMost('50px').wide
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("atMost('100px').tall passes when height <= max", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atMost('100px').tall
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("atMost('50px').tall fails when height > max", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atMost('50px').tall
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("between('50px','100px').wide passes when width in range", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.between('50px', '100px').wide
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("between('90px','100px').wide fails when width < min", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.between('90px', '100px').wide
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("between('50px','100px').tall passes when height in range", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.between('50px', '100px').tall
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test("between('90px','100px').tall fails when height < min", async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.between('90px', '100px').tall
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
// ---- New Size Assertion Overloads ----
|
||
|
|
|
||
|
|
test('atLeast(44, "width") passes when width >= 44', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atLeast(44, 'width')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('atLeast(100, "width") fails when width < 100', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atLeast(100, 'width')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('atLeast({ width: 44 }) passes when width >= 44', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atLeast({ width: 44 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('atMost(100, "height") passes when height <= 100', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atMost(100, 'height')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('atMost(50, "height") fails when height > 50', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.atMost(50, 'height')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('between(50, 100, "width") passes when width in range', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.between(50, 100, 'width')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('between(90, 100, "width") fails when width < min', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.between(90, 100, 'width')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('between({ width: [50, 100] }) passes when width in range', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.between({ width: [50, 100] })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('contains passes when subject encloses reference', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="contains-container"]').to.be.contains('[data-testid="box-contained"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('contains fails when subject does not enclose reference', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-contained"]').to.be.contains('[data-testid="contains-container"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('overlaps passes when elements intersect', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="overlap-a"]').to.be.overlaps('[data-testid="overlap-b"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('overlaps fails when elements are separated', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="overlap-a"]').to.be.overlaps('[data-testid="overlap-separate"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('failing page relation prints stable trace metadata (sourceRef + clauseLabel)', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.leftOf('[data-testid="box-right"]', { minGap: 20 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults.length).toBe(1)
|
||
|
|
|
||
|
|
const cr = result.clauseResults[0]
|
||
|
|
expect(cr.status).toBe('fail')
|
||
|
|
|
||
|
|
// sourceRef must be present with fluentIndex for fluent assertions
|
||
|
|
expect(cr.sourceRef).toBeDefined()
|
||
|
|
expect(cr.sourceRef!.fluentIndex).toBe(0)
|
||
|
|
expect(cr.sourceRef!.specLine).toBeUndefined()
|
||
|
|
|
||
|
|
// clauseLabel must be a human-readable string describing the contract
|
||
|
|
expect(cr.clauseLabel).toBeDefined()
|
||
|
|
expect(typeof cr.clauseLabel).toBe('string')
|
||
|
|
expect(cr.clauseLabel).toContain('leftOf')
|
||
|
|
expect(cr.clauseLabel).toContain('[data-testid="box-left"]')
|
||
|
|
expect(cr.clauseLabel).toContain('[data-testid="box-right"]')
|
||
|
|
|
||
|
|
// Diagnostics should also carry traceability
|
||
|
|
const diag = result.diagnostics.find((d: { code: string }) => d.code === 'IMH_RELATION_LEFT_OF_FAILED')
|
||
|
|
expect(diag).toBeDefined()
|
||
|
|
expect(diag!.clauseLabel).toBeDefined()
|
||
|
|
expect(diag!.sourceRef).toBeDefined()
|
||
|
|
expect((diag!.sourceRef as { fluentIndex?: number }).fluentIndex).toBe(0)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('dense spec assertion carries specLine/specColumn in E2E', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.spec("'[data-testid=\"box-left\"]' leftOf '[data-testid=\"box-right\"]' gap 20px")
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults.length).toBe(1)
|
||
|
|
|
||
|
|
const cr = result.clauseResults[0]
|
||
|
|
expect(cr.sourceRef).toBeDefined()
|
||
|
|
expect(cr.sourceRef!.fluentIndex).toBeUndefined()
|
||
|
|
expect(typeof cr.sourceRef!.specLine).toBe('number')
|
||
|
|
expect(typeof cr.sourceRef!.specColumn).toBe('number')
|
||
|
|
expect(cr.sourceRef!.specLine).toBeGreaterThanOrEqual(1)
|
||
|
|
expect(cr.sourceRef!.specColumn).toBeGreaterThanOrEqual(1)
|
||
|
|
|
||
|
|
expect(cr.clauseLabel).toBeDefined()
|
||
|
|
expect(cr.clauseLabel).toContain('leftOf')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('mixed fluent + dense batch maps each failure to exact authored assertion in E2E', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.leftOf('[data-testid="box-right"]', { minGap: 20 })
|
||
|
|
ui.spec("'[data-testid=\"box-above\"]' above '[data-testid=\"box-below\"]' gap 20px")
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults.length).toBe(2)
|
||
|
|
|
||
|
|
// Fluent assertion comes first in processing order
|
||
|
|
const fluentCr = result.clauseResults[0]
|
||
|
|
expect(fluentCr.sourceRef!.fluentIndex).toBe(0)
|
||
|
|
expect(fluentCr.sourceRef!.specLine).toBeUndefined()
|
||
|
|
expect(fluentCr.clauseLabel).toContain('leftOf')
|
||
|
|
|
||
|
|
// Dense spec comes second
|
||
|
|
const denseCr = result.clauseResults[1]
|
||
|
|
expect(denseCr.sourceRef!.fluentIndex).toBeUndefined()
|
||
|
|
expect(typeof denseCr.sourceRef!.specLine).toBe('number')
|
||
|
|
expect(denseCr.clauseLabel).toContain('above')
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
test.describe('E2E: Selector Cardinality Contracts (P2.1)', () => {
|
||
|
|
test('exactlyOne passes when selector matches exactly 1 element', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="size-box"]').to.be.exactlyOne()
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults.length).toBe(1)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
expect(result.clauseResults[0].metrics.observedCount).toBe(1)
|
||
|
|
expect(result.clauseResults[0].metrics.expectedCount).toBe(1)
|
||
|
|
expect(result.clauseResults[0].metrics.selector).toBe('[data-testid="size-box"]')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('exactlyOne fails when selector matches 0 elements', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('.does-not-exist').to.be.exactlyOne()
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
const cardClause = result.clauseResults.find((cr: any) => cr.metrics?.expectedCount === 1)
|
||
|
|
expect(cardClause).toBeDefined()
|
||
|
|
expect(cardClause!.status).toBe('fail')
|
||
|
|
expect(cardClause!.metrics.observedCount).toBe(0)
|
||
|
|
expect(cardClause!.metrics.selector).toBe('.does-not-exist')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('exactlyOne fails when selector matches multiple elements', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('multi-button'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('.button').to.be.exactlyOne()
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
const cardClause = result.clauseResults.find((cr: any) => cr.metrics?.expectedCount === 1)
|
||
|
|
expect(cardClause).toBeDefined()
|
||
|
|
expect(cardClause!.status).toBe('fail')
|
||
|
|
expect(cardClause!.metrics.observedCount).toBe(3)
|
||
|
|
expect(cardClause!.metrics.selector).toBe('.button')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('atLeastN passes when count >= n', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('multi-button'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('.button').to.be.atLeastN(2)
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
const cardClause = result.clauseResults.find((cr: any) => cr.metrics?.expectedCount === 2)
|
||
|
|
expect(cardClause).toBeDefined()
|
||
|
|
expect(cardClause!.status).toBe('pass')
|
||
|
|
expect(cardClause!.metrics.observedCount).toBe(3)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('atLeastN fails when count < n', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('multi-button'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('.button').to.be.atLeastN(5)
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
const cardClause = result.clauseResults.find((cr: any) => cr.metrics?.expectedCount === 5)
|
||
|
|
expect(cardClause).toBeDefined()
|
||
|
|
expect(cardClause!.status).toBe('fail')
|
||
|
|
expect(cardClause!.metrics.observedCount).toBe(3)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('atMostN passes when count <= n', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('multi-button'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('.button').to.be.atMostN(5)
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
const cardClause = result.clauseResults.find((cr: any) => cr.metrics?.expectedCount === 5)
|
||
|
|
expect(cardClause).toBeDefined()
|
||
|
|
expect(cardClause!.status).toBe('pass')
|
||
|
|
expect(cardClause!.metrics.observedCount).toBe(3)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('atMostN fails when count > n', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('multi-button'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('.button').to.be.atMostN(2)
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
const cardClause = result.clauseResults.find((cr: any) => cr.metrics?.expectedCount === 2)
|
||
|
|
expect(cardClause).toBeDefined()
|
||
|
|
expect(cardClause!.status).toBe('fail')
|
||
|
|
expect(cardClause!.metrics.observedCount).toBe(3)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('cardinality failure includes diagnostic with selector and counts', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('multi-button'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('.button').to.be.exactlyOne()
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.diagnostics.length).toBeGreaterThan(0)
|
||
|
|
|
||
|
|
const diag = result.diagnostics.find((d: any) => d.code === 'IMH_CARDINALITY_EXACTLYONE_FAILED')
|
||
|
|
expect(diag).toBeDefined()
|
||
|
|
expect(diag!.message).toContain('.button')
|
||
|
|
expect(diag!.message).toContain('3')
|
||
|
|
expect(diag!.message).toContain('exactly 1')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('mixed batch: cardinality + spatial assertions evaluate together', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.exactlyOne()
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.leftOf('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults.length).toBe(2)
|
||
|
|
const cardClause = result.clauseResults.find((cr: any) => cr.metrics?.expectedCount === 1)
|
||
|
|
const spatialClause = result.clauseResults.find((cr: any) => cr.status === 'pass' && !cr.metrics?.expectedCount)
|
||
|
|
expect(cardClause).toBeDefined()
|
||
|
|
expect(spatialClause).toBeDefined()
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
test.describe('E2E: Spatial Alias Relations', () => {
|
||
|
|
test('beside passes when subject is leftOf reference', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.beside('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('beside passes when subject is rightOf reference', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-right"]').to.be.beside('[data-testid="box-left"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('beside fails when subject is not horizontally adjacent', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-above"]').to.be.beside('[data-testid="box-below"]', { minGap: 0, maxGap: 5 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
expect(result.diagnostics.some((d: any) => d.code === 'IMH_RELATION_BESIDE_FAILED')).toBe(true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('nextTo is alias for beside and passes', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.nextTo('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('under is alias for below and passes', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-below"]').to.be.under('[data-testid="box-above"]', { minGap: 8 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('within is alias for inside and passes', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-inside"]').to.be.within('[data-testid="container-inside"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('within fails when subject overflows reference', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="container-inside"]').to.be.within('[data-testid="box-inside"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('near passes for overlapping elements', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="overlap-a"]').to.be.near('[data-testid="overlap-b"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('near passes for proximate non-overlapping elements', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
// box-left and box-right have a 10px gap, within default radius 100
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.near('[data-testid="box-right"]')
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults[0].status).toBe('pass')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('near fails when elements are far apart', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
ui.expect('[data-testid="box-left"]').to.be.near('[data-testid="overlap-separate"]', { maxGap: 5 })
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(false)
|
||
|
|
expect(result.clauseResults[0].status).toBe('fail')
|
||
|
|
expect(result.diagnostics.some((d: any) => d.code === 'IMH_RELATION_NEAR_FAILED')).toBe(true)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
test.describe('E2E: Fluent FOL Quantifiers', () => {
|
||
|
|
test('forAll with leftOf predicate passes when all items satisfy relation', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
const { FluentAssertion } = await import('imhotep-dsl')
|
||
|
|
const quantifier = FluentAssertion.forAll('[data-testid="box-left"]', (el) =>
|
||
|
|
el.expect().to.be.leftOf('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
)
|
||
|
|
|
||
|
|
ui.quantifier(quantifier)
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('exists with above predicate passes when at least one item satisfies', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
const { FluentAssertion } = await import('imhotep-dsl')
|
||
|
|
const quantifier = FluentAssertion.exists('[data-testid="box-above"]', (el) =>
|
||
|
|
el.expect().to.be.above('[data-testid="box-below"]', { minGap: 8 })
|
||
|
|
)
|
||
|
|
|
||
|
|
ui.quantifier(quantifier)
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('nested quantifiers compile and evaluate through adapter', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
const { FluentAssertion } = await import('imhotep-dsl')
|
||
|
|
const quantifier = FluentAssertion.forAll('[data-testid="box-left"]', (el) =>
|
||
|
|
el.expect().to.be.leftOf('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
)
|
||
|
|
|
||
|
|
ui.quantifier(quantifier)
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('fluent FOL quantifier carries sourceRef and clauseLabel', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
const { FluentAssertion } = await import('imhotep-dsl')
|
||
|
|
const quantifier = FluentAssertion.forAll('[data-testid="box-left"]', (el) =>
|
||
|
|
el.expect().to.be.leftOf('[data-testid="box-right"]', { minGap: 8 })
|
||
|
|
)
|
||
|
|
|
||
|
|
ui.quantifier(quantifier)
|
||
|
|
|
||
|
|
const result = await ui.checkAll()
|
||
|
|
expect(result.passed).toBe(true)
|
||
|
|
expect(result.clauseResults.length).toBe(1)
|
||
|
|
expect(result.clauseResults[0].sourceRef).toBeDefined()
|
||
|
|
expect(result.clauseResults[0].clauseLabel).toBeDefined()
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
test.describe('E2E: applyEnvironment — full environment axes', () => {
|
||
|
|
test('applyEnvironment exposes colorScheme axis (dark mode)', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
// Apply dark color scheme via the public API
|
||
|
|
await ui.applyEnvironment({
|
||
|
|
viewport: { width: 800, height: 600 },
|
||
|
|
colorScheme: 'dark',
|
||
|
|
})
|
||
|
|
|
||
|
|
// Verify the page reflects the dark mode emulation
|
||
|
|
const isDark = await page.evaluate(() =>
|
||
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||
|
|
)
|
||
|
|
expect(isDark).toBe(true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('applyEnvironment exposes reducedMotion axis', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
// Apply reduced motion preference
|
||
|
|
await ui.applyEnvironment({
|
||
|
|
viewport: { width: 800, height: 600 },
|
||
|
|
reducedMotion: 'reduce',
|
||
|
|
})
|
||
|
|
|
||
|
|
// Verify reduced motion is active via media query or injected style
|
||
|
|
const motionReduced = await page.evaluate(() => {
|
||
|
|
// Check if the injected style element exists (CSS injection fallback path)
|
||
|
|
const style = document.getElementById('__imhotep-reduced-motion__')
|
||
|
|
if (style) return true
|
||
|
|
// Otherwise rely on native emulation
|
||
|
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||
|
|
})
|
||
|
|
expect(motionReduced).toBe(true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('applyEnvironment exposes pointerType axis (coarse)', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
// Apply coarse pointer type
|
||
|
|
await ui.applyEnvironment({
|
||
|
|
viewport: { width: 800, height: 600 },
|
||
|
|
pointerType: 'coarse',
|
||
|
|
})
|
||
|
|
|
||
|
|
// Verify the CSS custom property was set by the implementation
|
||
|
|
const pointerType = await page.evaluate(() =>
|
||
|
|
getComputedStyle(document.documentElement).getPropertyValue('--imhotep-pointer-type').trim()
|
||
|
|
)
|
||
|
|
expect(pointerType).toBe('coarse')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('applyEnvironment keeps backward compat with viewport-only calls', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
// Viewport-only call must still compile and execute without error
|
||
|
|
await ui.applyEnvironment({
|
||
|
|
viewport: { width: 1024, height: 768 },
|
||
|
|
})
|
||
|
|
|
||
|
|
const size = await page.viewportSize()
|
||
|
|
expect(size).toEqual({ width: 1024, height: 768 })
|
||
|
|
})
|
||
|
|
|
||
|
|
test('applyEnvironment applies multiple axes in one call', async ({ page }) => {
|
||
|
|
const ui = await imhotep(page)
|
||
|
|
await page.goto(await fixtureUrl('public-api'))
|
||
|
|
await waitForFixtureReady(page)
|
||
|
|
|
||
|
|
// Apply multiple axes simultaneously
|
||
|
|
await ui.applyEnvironment({
|
||
|
|
viewport: { width: 1280, height: 720 },
|
||
|
|
colorScheme: 'dark',
|
||
|
|
reducedMotion: 'reduce',
|
||
|
|
pointerType: 'fine',
|
||
|
|
deviceScaleFactor: 2,
|
||
|
|
locale: 'en-US',
|
||
|
|
})
|
||
|
|
|
||
|
|
// Verify viewport
|
||
|
|
const size = await page.viewportSize()
|
||
|
|
expect(size).toEqual({ width: 1280, height: 720 })
|
||
|
|
|
||
|
|
// Verify color scheme
|
||
|
|
const isDark = await page.evaluate(() =>
|
||
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||
|
|
)
|
||
|
|
expect(isDark).toBe(true)
|
||
|
|
|
||
|
|
// Verify locale
|
||
|
|
const lang = await page.evaluate(() => document.documentElement.lang)
|
||
|
|
expect(lang).toBe('en-US')
|
||
|
|
|
||
|
|
// Verify pointer type
|
||
|
|
const pointerType = await page.evaluate(() =>
|
||
|
|
getComputedStyle(document.documentElement).getPropertyValue('--imhotep-pointer-type').trim()
|
||
|
|
)
|
||
|
|
expect(pointerType).toBe('fine')
|
||
|
|
})
|
||
|
|
})
|