feat: add 50% threshold for diffing - show full content when changes are large
When showDiffSinceLastCall detects >50% of lines changed, returns full new content instead of a diff patch. This makes large changes more readable.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Page, Locator } from 'playwright-core'
|
||||
import { createPatch } from 'diff'
|
||||
import { formatHtmlForPrompt } from './htmlrewrite.js'
|
||||
import { createSmartDiff } from './diff-utils.js'
|
||||
|
||||
export interface GetCleanHTMLOptions {
|
||||
locator: Locator | Page
|
||||
@@ -81,16 +81,15 @@ export async function getCleanHTML(options: GetCleanHTMLOptions): Promise<string
|
||||
return 'No previous snapshot available. This is the first call for this locator. Full snapshot stored for next diff.'
|
||||
}
|
||||
|
||||
const patch = createPatch('html', previousSnapshot, htmlStr, 'previous', 'current', {
|
||||
context: 3,
|
||||
const diffResult = createSmartDiff({
|
||||
oldContent: previousSnapshot,
|
||||
newContent: htmlStr,
|
||||
label: 'html',
|
||||
})
|
||||
|
||||
pageSnapshots.set(snapshotKey, htmlStr)
|
||||
|
||||
if (patch.split('\n').length <= 4) {
|
||||
return 'No changes detected since last snapshot'
|
||||
}
|
||||
return patch
|
||||
return diffResult.content
|
||||
}
|
||||
|
||||
// Store snapshot for future diffs
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { createSmartDiff } from './diff-utils.js'
|
||||
|
||||
describe('createSmartDiff', () => {
|
||||
it('returns no-change when content is identical', () => {
|
||||
const result = createSmartDiff({
|
||||
oldContent: 'hello\nworld',
|
||||
newContent: 'hello\nworld',
|
||||
})
|
||||
expect(result.type).toBe('no-change')
|
||||
expect(result.content).toBe('No changes detected since last snapshot')
|
||||
})
|
||||
|
||||
it('returns diff for small changes', () => {
|
||||
const result = createSmartDiff({
|
||||
oldContent: 'line1\nline2\nline3\nline4\nline5',
|
||||
newContent: 'line1\nline2-modified\nline3\nline4\nline5',
|
||||
})
|
||||
expect(result.type).toBe('diff')
|
||||
expect(result.content).toContain('-line2')
|
||||
expect(result.content).toContain('+line2-modified')
|
||||
})
|
||||
|
||||
it('returns full content when changes exceed 50% threshold', () => {
|
||||
const result = createSmartDiff({
|
||||
oldContent: 'a\nb\nc\nd',
|
||||
newContent: 'x\ny\nz\nw',
|
||||
})
|
||||
expect(result.type).toBe('full')
|
||||
expect(result.content).toContain('Content changed significantly')
|
||||
expect(result.content).toContain('x\ny\nz\nw')
|
||||
})
|
||||
|
||||
it('respects custom threshold', () => {
|
||||
// 2 out of 4 lines changed = 50%
|
||||
const oldContent = 'a\nb\nc\nd'
|
||||
const newContent = 'a\nX\nY\nd'
|
||||
|
||||
// With 40% threshold, should return full (50% >= 40%)
|
||||
const resultLow = createSmartDiff({
|
||||
oldContent,
|
||||
newContent,
|
||||
threshold: 0.4,
|
||||
})
|
||||
expect(resultLow.type).toBe('full')
|
||||
|
||||
// With 60% threshold, should return diff (50% < 60%)
|
||||
const resultHigh = createSmartDiff({
|
||||
oldContent,
|
||||
newContent,
|
||||
threshold: 0.6,
|
||||
})
|
||||
expect(resultHigh.type).toBe('diff')
|
||||
})
|
||||
|
||||
it('uses custom label in diff output', () => {
|
||||
const result = createSmartDiff({
|
||||
oldContent: 'line1\nline2\nline3\nline4\nline5',
|
||||
newContent: 'line1\nmodified\nline3\nline4\nline5',
|
||||
label: 'snapshot',
|
||||
threshold: 0.5, // 20% change, will return diff
|
||||
})
|
||||
expect(result.type).toBe('diff')
|
||||
expect(result.content).toContain('--- snapshot')
|
||||
expect(result.content).toContain('+++ snapshot')
|
||||
})
|
||||
|
||||
it('handles empty old content (all additions)', () => {
|
||||
const result = createSmartDiff({
|
||||
oldContent: '',
|
||||
newContent: 'new\ncontent\nhere',
|
||||
})
|
||||
// 100% new content should trigger full
|
||||
expect(result.type).toBe('full')
|
||||
expect(result.content).toContain('new\ncontent\nhere')
|
||||
})
|
||||
|
||||
it('handles empty new content (all deletions)', () => {
|
||||
const result = createSmartDiff({
|
||||
oldContent: 'old\ncontent\nhere',
|
||||
newContent: '',
|
||||
})
|
||||
expect(result.type).toBe('full')
|
||||
expect(result.content).toContain('100%')
|
||||
})
|
||||
|
||||
it('handles single line changes', () => {
|
||||
const result = createSmartDiff({
|
||||
oldContent: 'single line',
|
||||
newContent: 'modified line',
|
||||
})
|
||||
// 100% change on single line
|
||||
expect(result.type).toBe('full')
|
||||
})
|
||||
|
||||
it('calculates percentage correctly in output', () => {
|
||||
// 4 lines changed out of 5 = 80%
|
||||
const result = createSmartDiff({
|
||||
oldContent: 'a\nb\nc\nd\ne',
|
||||
newContent: 'a\nX\nY\nZ\nW',
|
||||
})
|
||||
expect(result.type).toBe('full')
|
||||
expect(result.content).toContain('(80% of lines)')
|
||||
})
|
||||
|
||||
it('includes context lines in diff output', () => {
|
||||
const result = createSmartDiff({
|
||||
oldContent: 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10',
|
||||
newContent: 'line1\nline2\nline3\nline4\nMODIFIED\nline6\nline7\nline8\nline9\nline10',
|
||||
threshold: 0.9, // Force diff
|
||||
})
|
||||
expect(result.type).toBe('diff')
|
||||
// Should have context around the change
|
||||
expect(result.content).toContain('line4')
|
||||
expect(result.content).toContain('-line5')
|
||||
expect(result.content).toContain('+MODIFIED')
|
||||
expect(result.content).toContain('line6')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { structuredPatch } from 'diff'
|
||||
|
||||
export interface SmartDiffResult {
|
||||
type: 'diff' | 'full' | 'no-change'
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface CreateSmartDiffOptions {
|
||||
oldContent: string
|
||||
newContent: string
|
||||
/** Threshold ratio (0-1) above which full content is returned instead of diff. Default 0.5 (50%) */
|
||||
threshold?: number
|
||||
/** Label for the diff output */
|
||||
label?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a smart diff that returns full content when changes exceed threshold.
|
||||
*
|
||||
* When more than `threshold` (default 50%) of lines have changed, showing a diff
|
||||
* is not useful - we return the full new content instead.
|
||||
*/
|
||||
export function createSmartDiff(options: CreateSmartDiffOptions): SmartDiffResult {
|
||||
const { oldContent, newContent, threshold = 0.5, label = 'content' } = options
|
||||
|
||||
const patch = structuredPatch(label, label, oldContent, newContent, 'previous', 'current', {
|
||||
context: 3,
|
||||
})
|
||||
|
||||
// Count added and removed lines
|
||||
let addedLines = 0
|
||||
let removedLines = 0
|
||||
for (const hunk of patch.hunks) {
|
||||
for (const line of hunk.lines) {
|
||||
if (line.startsWith('+')) {
|
||||
addedLines++
|
||||
} else if (line.startsWith('-')) {
|
||||
removedLines++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No changes
|
||||
if (addedLines === 0 && removedLines === 0) {
|
||||
return { type: 'no-change', content: 'No changes detected since last snapshot' }
|
||||
}
|
||||
|
||||
// Calculate change ratio: use max(added, removed) since a replacement counts as both
|
||||
// This ensures the ratio stays in 0-100% range
|
||||
const oldLineCount = oldContent.split('\n').length
|
||||
const newLineCount = newContent.split('\n').length
|
||||
const maxLines = Math.max(oldLineCount, newLineCount, 1) // Avoid division by zero
|
||||
const changedLines = Math.max(addedLines, removedLines)
|
||||
const changeRatio = Math.min(changedLines / maxLines, 1) // Cap at 100%
|
||||
|
||||
if (changeRatio >= threshold) {
|
||||
const percentChanged = Math.round(changeRatio * 100)
|
||||
return {
|
||||
type: 'full',
|
||||
content: `Content changed significantly (${percentChanged}% of lines). Full new content:\n\n${newContent}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Build unified diff string from structured patch
|
||||
const diffLines: string[] = [
|
||||
`--- ${label} (previous)`,
|
||||
`+++ ${label} (current)`,
|
||||
]
|
||||
for (const hunk of patch.hunks) {
|
||||
diffLines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`)
|
||||
diffLines.push(...hunk.lines)
|
||||
}
|
||||
|
||||
return { type: 'diff', content: diffLines.join('\n') }
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import util from 'node:util'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import vm from 'node:vm'
|
||||
import { createPatch } from 'diff'
|
||||
import { createSmartDiff } from './diff-utils.js'
|
||||
import { getCdpUrl } from './utils.js'
|
||||
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
|
||||
import { getCDPSessionForPage, CDPSession, ICDPSession } from './cdp-session.js'
|
||||
@@ -427,11 +427,13 @@ export class PlaywrightExecutor {
|
||||
this.lastSnapshots.set(targetPage, snapshotStr)
|
||||
return 'No previous snapshot available. This is the first call for this page. Full snapshot stored for next diff.'
|
||||
}
|
||||
const patch = createPatch('snapshot', previousSnapshot, snapshotStr, 'previous', 'current', { context: 3 })
|
||||
if (patch.split('\n').length <= 4) {
|
||||
return 'No changes detected since last snapshot'
|
||||
}
|
||||
return patch
|
||||
const diffResult = createSmartDiff({
|
||||
oldContent: previousSnapshot,
|
||||
newContent: snapshotStr,
|
||||
label: 'snapshot',
|
||||
})
|
||||
this.lastSnapshots.set(targetPage, snapshotStr)
|
||||
return diffResult.content
|
||||
}
|
||||
|
||||
this.lastSnapshots.set(targetPage, snapshotStr)
|
||||
|
||||
Reference in New Issue
Block a user