test: add unit tests for test-recorder transform and improve README

- Add 25 colocated unit tests for transform rules and engine
- Add tools/ to vitest include pattern
- Improve README with quick-start prereqs and test instructions
This commit is contained in:
bymyself
2026-04-06 14:25:01 -07:00
parent 9faccf5c18
commit 0959435d53
4 changed files with 263 additions and 6 deletions

View File

@@ -2,15 +2,15 @@
Interactive CLI for recording and transforming Playwright browser tests for ComfyUI.
## Usage
## Quick Start
From the repo root:
**Prerequisites:** Node.js ≥ 20, pnpm, a running ComfyUI backend. See the [Browser Tests README](../../browser_tests/README.md) for detailed environment setup including Playwright installation and backend configuration.
```bash
pnpm comfy-test record # Record a new test
pnpm comfy-test check # Verify your environment is ready
pnpm comfy-test record # Record a new test
pnpm comfy-test transform <file> # Transform raw codegen to conventions
pnpm comfy-test check # Check environment prerequisites
pnpm comfy-test list # List available workflows
pnpm comfy-test list # List available workflows
```
## For QA Testers
@@ -24,3 +24,9 @@ cd tools/test-recorder
pnpm build # Compile TypeScript
pnpm dev # Watch mode
```
Run unit tests from the repo root:
```bash
pnpm test:unit -- tools/test-recorder
```

View File

@@ -0,0 +1,109 @@
import { describe, expect, it } from 'vitest'
import { transform, formatTransformSummary } from './engine'
describe('transform', () => {
const rawCodegenOutput = `import { test, expect } from '@playwright/test'
test('my test', async ({ page }) => {
await page.goto('http://localhost:8188')
await page.locator('canvas').click()
await page.waitForTimeout(1000)
await page.getByPlaceholder('Search Nodes...').fill('KSampler')
})`
it('applies all applicable regex rules', () => {
const result = transform(rawCodegenOutput, {
testName: 'canvas-test',
tags: ['@canvas']
})
expect(result.code).toContain('comfyPageFixture as test')
expect(result.code).toContain('async ({ comfyPage })')
expect(result.code).not.toContain('page.goto')
expect(result.code).toContain('comfyPage.canvas')
expect(result.code).toContain('comfyPage.nextFrame()')
expect(result.code).toContain('comfyPage.searchBox.input')
})
it('wraps test in describe block', () => {
const result = transform(rawCodegenOutput, {
testName: 'canvas-test',
tags: ['@canvas']
})
expect(result.code).toContain('test.describe(')
expect(result.code).toContain('"canvas test"')
})
it('tracks applied rules', () => {
const result = transform(rawCodegenOutput, { testName: 'test' })
const ruleNames = result.appliedRules.map((r) => r.name)
expect(ruleNames).toContain('replace-test-import')
expect(ruleNames).toContain('replace-page-destructure')
expect(ruleNames).toContain('remove-goto')
expect(ruleNames).toContain('replace-canvas-locator')
expect(ruleNames).toContain('replace-waitForTimeout')
expect(ruleNames).toContain('wrap-in-describe')
})
it('warns about remaining pixel coordinates', () => {
const input = `import { test } from '@playwright/test'
test('pos test', async ({ page }) => {
await page.click({ position: { x: 100, y: 200 } })
})`
const result = transform(input)
expect(result.warnings).toContainEqual(
expect.stringContaining('pixel coordinates')
)
})
it('uses default testName and tags when not provided', () => {
const result = transform(rawCodegenOutput)
expect(result.code).toContain('"unnamed test"')
expect(result.code).toContain('"@canvas"')
})
it('collapses triple blank lines', () => {
const input = `import { test } from '@playwright/test'
test('x', async ({ page }) => {})`
const result = transform(input)
expect(result.code).not.toMatch(/\n{3,}/)
})
it('returns code ending with a single newline', () => {
const result = transform(rawCodegenOutput)
expect(result.code).toMatch(/[^\n]\n$/)
})
})
describe('formatTransformSummary', () => {
it('formats applied rules with checkmarks', () => {
const lines = formatTransformSummary({
code: '',
appliedRules: [{ name: 'test-rule', description: 'Did a thing' }],
warnings: []
})
expect(lines).toEqual(['✅ Did a thing'])
})
it('formats warnings', () => {
const lines = formatTransformSummary({
code: '',
appliedRules: [],
warnings: ['Something is wrong']
})
expect(lines).toEqual(['⚠️ Something is wrong'])
})
it('returns empty array when no rules or warnings', () => {
const lines = formatTransformSummary({
code: '',
appliedRules: [],
warnings: []
})
expect(lines).toEqual([])
})
})

View File

@@ -0,0 +1,141 @@
import { describe, expect, it } from 'vitest'
import { transformRules, structuralTransforms } from './rules'
describe('transformRules', () => {
function applyRule(ruleName: string, input: string): string {
const rule = transformRules.find((r) => r.name === ruleName)
if (!rule) throw new Error(`Rule not found: ${ruleName}`)
if (typeof rule.replacement === 'string') {
return input.replace(rule.pattern, rule.replacement)
}
return input.replace(
rule.pattern,
rule.replacement as (...args: string[]) => string
)
}
describe('import transforms', () => {
it('replaces { test, expect } from @playwright/test', () => {
const input = `import { test, expect } from '@playwright/test'`
const result = applyRule('replace-test-import', input)
expect(result).toContain('comfyPageFixture as test')
expect(result).toContain('comfyExpect as expect')
expect(result).toContain("from '../fixtures/ComfyPage'")
})
it('replaces { expect, test } (reversed order)', () => {
const input = `import { expect, test } from '@playwright/test'`
const result = applyRule('replace-test-import', input)
expect(result).toContain('comfyPageFixture as test')
})
it('replaces test-only import', () => {
const input = `import { test } from '@playwright/test'`
const result = applyRule('replace-test-only-import', input)
expect(result).toContain('comfyPageFixture as test')
expect(result).not.toContain('expect')
})
it('replaces expect-only import', () => {
const input = `import { expect } from '@playwright/test'`
const result = applyRule('replace-expect-only-import', input)
expect(result).toContain('comfyExpect as expect')
expect(result).not.toContain('comfyPageFixture')
})
})
describe('fixture transforms', () => {
it('replaces { page } with { comfyPage }', () => {
const input = `test('my test', async ({ page }) => {`
const result = applyRule('replace-page-destructure', input)
expect(result).toContain('async ({ comfyPage })')
expect(result).not.toContain('{ page }')
})
})
describe('locator transforms', () => {
it('removes page.goto calls', () => {
const input = ` await page.goto('http://localhost:8188')\n await page.click('button')`
const result = applyRule('remove-goto', input)
expect(result).not.toContain('page.goto')
expect(result).toContain('page.click')
})
it('replaces page.locator("canvas")', () => {
const input = `await page.locator('canvas').click()`
const result = applyRule('replace-canvas-locator', input)
expect(result).toBe('await comfyPage.canvas.click()')
})
it('replaces search box placeholder', () => {
const input = `page.getByPlaceholder('Search Nodes...')`
const result = applyRule('replace-search-placeholder', input)
expect(result).toBe('comfyPage.searchBox.input')
})
it('replaces bare page. references with comfyPage.page.', () => {
const input = `await page.click('button')`
const result = applyRule('replace-bare-page', input)
expect(result).toBe(`await comfyPage.page.click('button')`)
})
it('does not replace comfyPage.page. (no double-replace)', () => {
const input = `await comfyPage.page.click('button')`
const result = applyRule('replace-bare-page', input)
expect(result).toBe(input)
})
})
describe('wait transforms', () => {
it('replaces waitForTimeout with nextFrame', () => {
const input = `await page.waitForTimeout(1000);`
const result = applyRule('replace-waitForTimeout', input)
expect(result).toBe('await comfyPage.nextFrame()')
})
it('handles waitForTimeout without semicolon', () => {
const input = `await page.waitForTimeout(500)`
const result = applyRule('replace-waitForTimeout', input)
expect(result).toBe('await comfyPage.nextFrame()')
})
})
})
describe('structuralTransforms', () => {
const wrapInDescribe = structuralTransforms.find(
(t) => t.name === 'wrap-in-describe'
)!
it('wraps a test in test.describe with tags', () => {
const input = `import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test('does something', async ({ comfyPage }) => {
await comfyPage.canvas.click()
})`
const result = wrapInDescribe.apply(input, 'my-test', ['@canvas'])
expect(result).toContain('test.describe(')
expect(result).toContain('"my test"')
expect(result).toContain('"@canvas"')
expect(result).toContain('test.afterEach')
expect(result).toContain('resetView')
})
it('skips wrapping when test.describe already exists', () => {
const input = `test.describe('existing', () => {
test('inner', async ({ comfyPage }) => {})
})`
const result = wrapInDescribe.apply(input, 'test', ['@canvas'])
expect(result).toBe(input)
})
it('converts hyphens and underscores to spaces in describe name', () => {
const input = `import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test('x', async ({ comfyPage }) => {})`
const result = wrapInDescribe.apply(input, 'my_test-name', ['@canvas'])
expect(result).toContain('"my test name"')
})
})

View File

@@ -650,7 +650,8 @@ export default defineConfig({
include: [
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'packages/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
'scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'tools/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
],
coverage: {
reporter: ['text', 'json', 'html']