Skip to content

Commit 1872783

Browse files
committed
test
1 parent f2280b9 commit 1872783

File tree

7 files changed

+73
-56
lines changed

7 files changed

+73
-56
lines changed

src/compare/libs/vlm/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ Optional custom prompt (replaces default system prompt):
5757
| Model | Size | Speed | Accuracy | Best For |
5858
|-------|------|-------|----------|----------|
5959
| `llava:7b` | 4.7GB | ⚡⚡ | ⭐⭐⭐ | **Recommended** - best balance (minimal) |
60-
| `qwen3-vl:8b` | ~8GB | ⚡⚡ | ⭐⭐⭐ | Minimal model option |
6160
| `gemma3:latest` | ~ | ⚡⚡ | ⭐⭐⭐ | Minimal model option |
6261
| `llava:13b` | 8GB || ⭐⭐⭐⭐ | Best accuracy |
6362
| `moondream` | 1.7GB | ⚡⚡⚡ | ⭐⭐ | Fast, may hallucinate |

src/compare/libs/vlm/vlm.service.spec.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@ import { NO_BASELINE_RESULT } from '../consts';
66
import { DEFAULT_CONFIG, VlmService } from './vlm.service';
77
import { OllamaService } from './ollama.service';
88

9-
const initService = async ({ getImageMock = jest.fn(), saveImageMock = jest.fn(), ollamaGenerateMock = jest.fn() }) => {
9+
const initService = async ({ getImageMock = jest.fn(), ollamaGenerateMock = jest.fn() }) => {
1010
const module: TestingModule = await Test.createTestingModule({
1111
providers: [
1212
VlmService,
1313
{
1414
provide: StaticService,
1515
useValue: {
1616
getImage: getImageMock,
17-
saveImage: saveImageMock,
1817
},
1918
},
2019
{
@@ -65,12 +64,11 @@ describe('VlmService', () => {
6564

6665
it('should return unresolved when VLM returns identical=false in JSON', async () => {
6766
const getImageMock = jest.fn().mockReturnValue(image);
68-
const saveImageMock = jest.fn().mockResolvedValue('diff.png');
6967
const ollamaGenerateMock = jest.fn().mockResolvedValue({
7068
response: '{"identical": false, "description": "Button text changed from Submit to Send."}',
7169
done: true,
7270
});
73-
const service = await initService({ getImageMock, saveImageMock, ollamaGenerateMock });
71+
const service = await initService({ getImageMock, ollamaGenerateMock });
7472

7573
const result = await service.getDiff(
7674
{ baseline: 'baseline', image: 'image', diffTollerancePercent: 0.1, ignoreAreas: [], saveDiffAsFile: true },
@@ -79,9 +77,9 @@ describe('VlmService', () => {
7977

8078
expect(result.status).toBe(TestStatus.unresolved);
8179
expect(result.vlmDescription).toBe('Button text changed from Submit to Send.');
82-
expect(result.diffName).toBe('diff.png');
83-
expect(result.pixelMisMatchCount).toBeDefined();
84-
expect(result.diffPercent).toBeDefined();
80+
expect(result.diffName).toBeNull();
81+
expect(result.pixelMisMatchCount).toBe(0);
82+
expect(result.diffPercent).toBe(0);
8583
});
8684

8785
it('should handle invalid JSON response as error', async () => {

src/compare/libs/vlm/vlm.service.ts

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { Injectable, Logger } from '@nestjs/common';
22
import { TestStatus } from '@prisma/client';
3-
import Pixelmatch from 'pixelmatch';
4-
import { PNG } from 'pngjs';
53
import { StaticService } from '../../../static/static.service';
64
import { DiffResult } from '../../../test-runs/diffResult';
7-
import { parseConfig, pngToBase64, scaleImageToSize } from '../../utils';
5+
import { parseConfig, pngToBase64 } from '../../utils';
86
import { NO_BASELINE_RESULT } from '../consts';
97
import { ImageComparator } from '../image-comparator.interface';
108
import { ImageCompareInput } from '../ImageCompareInput';
@@ -23,11 +21,21 @@ CHECK for differences:
2321
IGNORE rendering artifacts: anti-aliasing, shadows, 1-2px shifts.`;
2422

2523
// Internal constant - not exposed to user config to ensure consistent JSON output
26-
const JSON_FORMAT_INSTRUCTION = `
27-
Respond with JSON: {"identical": true/false, "description": "explanation"}
28-
- Set "identical": true if screenshots match or have only ignorable artifacts
29-
- Set "identical": false if meaningful differences exist
30-
- Always provide a brief description`;
24+
const JSON_FORMAT_INSTRUCTION = `CRITICAL: You must respond with ONLY valid JSON in this exact format:
25+
{"identical": Boolean, "description": String}
26+
27+
**JSON Schema Reference:**
28+
The JSON object MUST conform to the following schema:
29+
{
30+
"identical": <boolean>,
31+
"description": <string>
32+
}
33+
34+
**Requirements:**
35+
1. **"identical":** Must be a standard boolean (\`true\` or \`false\`).
36+
2. **"description":** Must be a detailed string explaining the reasoning.
37+
* If identical is \`true\`, the description should be "Screenshots are functionally identical based on all comparison criteria."
38+
* If identical is \`false\`, the description must clearly and concisely list the differences found (e.g., "The user count changed from 12 to 15, and the 'New User' button is missing."). Escape any internal double quotes with \\".`;
3139

3240
export const DEFAULT_CONFIG: VlmConfig = {
3341
model: 'llava:7b',
@@ -75,10 +83,9 @@ export class VlmService implements ImageComparator {
7583
result.diffName = null;
7684
} else {
7785
result.status = TestStatus.unresolved;
78-
const pixelDiff = this.calculatePixelDiff(baseline, image);
79-
result.pixelMisMatchCount = pixelDiff.pixelMisMatchCount;
80-
result.diffPercent = pixelDiff.diffPercent;
81-
result.diffName = data.saveDiffAsFile ? await this.saveDiffImage(baseline, image) : null;
86+
result.pixelMisMatchCount = 0;
87+
result.diffPercent = 0;
88+
result.diffName = null;
8289
}
8390
} catch (error) {
8491
this.logger.error(`VLM comparison failed: ${error.message}`, error.stack);
@@ -130,38 +137,4 @@ export class VlmService implements ImageComparator {
130137
description: parsed.description || 'No description provided',
131138
};
132139
}
133-
134-
private calculatePixelDiff(baseline: PNG, image: PNG): { pixelMisMatchCount: number; diffPercent: number } {
135-
const maxWidth = Math.max(baseline.width, image.width);
136-
const maxHeight = Math.max(baseline.height, image.height);
137-
const scaledBaseline = scaleImageToSize(baseline, maxWidth, maxHeight);
138-
const scaledImage = scaleImageToSize(image, maxWidth, maxHeight);
139-
140-
const diff = new PNG({ width: maxWidth, height: maxHeight });
141-
const pixelMisMatchCount = Pixelmatch(scaledBaseline.data, scaledImage.data, diff.data, maxWidth, maxHeight, {
142-
threshold: 0.1,
143-
includeAA: true,
144-
});
145-
146-
const diffPercent = Number(((pixelMisMatchCount * 100) / (maxWidth * maxHeight)).toFixed(2));
147-
this.logger.debug(`Pixelmatch: ${pixelMisMatchCount} pixels (${diffPercent}%)`);
148-
149-
return { pixelMisMatchCount, diffPercent };
150-
}
151-
152-
private async saveDiffImage(baseline: PNG, image: PNG): Promise<string> {
153-
const maxWidth = Math.max(baseline.width, image.width);
154-
const maxHeight = Math.max(baseline.height, image.height);
155-
const scaledBaseline = scaleImageToSize(baseline, maxWidth, maxHeight);
156-
const scaledImage = scaleImageToSize(image, maxWidth, maxHeight);
157-
158-
const diff = new PNG({ width: maxWidth, height: maxHeight });
159-
Pixelmatch(scaledBaseline.data, scaledImage.data, diff.data, maxWidth, maxHeight, {
160-
threshold: 0.1,
161-
includeAA: true,
162-
});
163-
164-
const diffBuffer = PNG.sync.write(diff);
165-
return this.staticService.saveImage('diff', diffBuffer);
166-
}
167140
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { TestRun } from '@prisma/client';
2+
import { generateTestRun } from '../../_data_';
3+
import { TestRunDto } from './testRun.dto';
4+
5+
describe('TestRunDto', () => {
6+
it('should map all fields correctly including vlmDescription', () => {
7+
const testRun: TestRun = generateTestRun({
8+
vlmDescription: 'VLM analysis result',
9+
});
10+
11+
const result = new TestRunDto(testRun);
12+
13+
expect(result).toMatchObject({
14+
id: testRun.id,
15+
buildId: testRun.buildId,
16+
imageName: testRun.imageName,
17+
diffName: testRun.diffName,
18+
diffPercent: testRun.diffPercent,
19+
diffTollerancePercent: testRun.diffTollerancePercent,
20+
status: testRun.status,
21+
testVariationId: testRun.testVariationId,
22+
name: testRun.name,
23+
baselineName: testRun.baselineName,
24+
os: testRun.os,
25+
browser: testRun.browser,
26+
viewport: testRun.viewport,
27+
device: testRun.device,
28+
customTags: testRun.customTags,
29+
ignoreAreas: testRun.ignoreAreas,
30+
tempIgnoreAreas: testRun.tempIgnoreAreas,
31+
comment: testRun.comment,
32+
branchName: testRun.branchName,
33+
baselineBranchName: testRun.baselineBranchName,
34+
merge: testRun.merge,
35+
vlmDescription: testRun.vlmDescription,
36+
});
37+
});
38+
});
39+

src/test-runs/dto/testRun.dto.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export class TestRunDto {
4444
baselineBranchName: string;
4545
@ApiProperty()
4646
merge: boolean;
47+
@ApiPropertyOptional()
48+
vlmDescription?: string;
4749

4850
constructor(testRun: TestRun) {
4951
this.id = testRun.id;
@@ -67,5 +69,6 @@ export class TestRunDto {
6769
this.branchName = testRun.branchName;
6870
this.baselineBranchName = testRun.baselineBranchName;
6971
this.merge = testRun.merge;
72+
this.vlmDescription = testRun.vlmDescription;
7073
}
7174
}

src/test-runs/test-runs.service.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ describe('TestRunsService', () => {
345345
diffName: null,
346346
pixelMisMatchCount: null,
347347
diffPercent: null,
348+
vlmDescription: null,
348349
},
349350
});
350351
expect(eventTestRunUpdatedMock).toHaveBeenCalledWith(testRun);
@@ -357,6 +358,7 @@ describe('TestRunsService', () => {
357358
pixelMisMatchCount: 11,
358359
diffPercent: 22,
359360
isSameDimension: true,
361+
vlmDescription: 'VLM detected significant color differences in the header section',
360362
};
361363
const id = 'some id';
362364
const testRunUpdateMock = jest.fn().mockResolvedValueOnce(testRun);
@@ -375,6 +377,7 @@ describe('TestRunsService', () => {
375377
diffName: diff.diffName,
376378
pixelMisMatchCount: diff.pixelMisMatchCount,
377379
diffPercent: diff.diffPercent,
380+
vlmDescription: diff.vlmDescription,
378381
},
379382
});
380383
expect(eventTestRunUpdatedMock).toHaveBeenCalledWith(testRun);
@@ -383,7 +386,9 @@ describe('TestRunsService', () => {
383386

384387
it('findMany', async () => {
385388
const buildId = 'some id';
386-
const testRun: TestRun = generateTestRun();
389+
const testRun: TestRun = generateTestRun({
390+
vlmDescription: 'VLM analysis completed',
391+
});
387392
const testRunFindManyMock = jest.fn().mockResolvedValueOnce([testRun]);
388393
service = await initService({
389394
testRunFindManyMock,

src/test-runs/test-runs.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export class TestRunsService {
173173
pixelMisMatchCount: diffResult && diffResult.pixelMisMatchCount,
174174
diffPercent: diffResult && diffResult.diffPercent,
175175
status: diffResult ? diffResult.status : TestStatus.new,
176-
vlmDescription: diffResult?.vlmDescription,
176+
vlmDescription: diffResult && diffResult?.vlmDescription,
177177
},
178178
})
179179
.then((testRun) => {

0 commit comments

Comments
 (0)