2024-08-15 17:59:43 +08:00
|
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
import { readFileSync } from 'node:fs';
|
2024-08-04 08:28:19 +08:00
|
|
|
import { tmpdir } from 'node:os';
|
2025-03-24 09:50:27 +08:00
|
|
|
import {
|
|
|
|
adaptBboxToRect,
|
|
|
|
adaptDoubaoBbox,
|
|
|
|
adaptQwenBbox,
|
|
|
|
expandSearchArea,
|
2025-04-02 19:26:56 +08:00
|
|
|
mergeRects,
|
2025-03-24 09:50:27 +08:00
|
|
|
} from '@/ai-model/common';
|
2025-01-14 21:07:03 +08:00
|
|
|
import {
|
|
|
|
extractJSONFromCodeBlock,
|
2025-03-17 19:19:54 +08:00
|
|
|
preprocessDoubaoBboxJson,
|
2025-01-14 21:07:03 +08:00
|
|
|
safeParseJson,
|
|
|
|
} from '@/ai-model/service-caller';
|
2025-03-24 09:50:27 +08:00
|
|
|
import { getAIConfig, overrideAIConfig, vlLocateMode } from '@/env';
|
2024-08-04 08:28:19 +08:00
|
|
|
import {
|
2024-08-15 17:59:43 +08:00
|
|
|
getLogDir,
|
2024-08-04 08:28:19 +08:00
|
|
|
getTmpDir,
|
|
|
|
getTmpFile,
|
|
|
|
overlapped,
|
2024-11-05 11:49:21 +08:00
|
|
|
reportHTMLContent,
|
2024-08-15 17:59:43 +08:00
|
|
|
setLogDir,
|
|
|
|
writeDumpReport,
|
2024-08-04 08:28:19 +08:00
|
|
|
} from '@/utils';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
2024-07-23 16:25:11 +08:00
|
|
|
|
|
|
|
describe('utils', () => {
|
|
|
|
it('tmpDir', () => {
|
|
|
|
const testDir = getTmpDir();
|
|
|
|
expect(typeof testDir).toBe('string');
|
2024-08-04 08:28:19 +08:00
|
|
|
|
2024-07-23 16:25:11 +08:00
|
|
|
const testFile = getTmpFile('txt');
|
2024-10-28 11:04:40 +08:00
|
|
|
expect(testFile!.endsWith('.txt')).toBe(true);
|
2024-07-23 16:25:11 +08:00
|
|
|
});
|
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
it('log dir', () => {
|
|
|
|
const dumpDir = getLogDir();
|
2024-07-23 16:25:11 +08:00
|
|
|
expect(dumpDir).toBeTruthy();
|
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
setLogDir(tmpdir());
|
|
|
|
const dumpDir2 = getLogDir();
|
2024-07-23 16:25:11 +08:00
|
|
|
expect(dumpDir2).toBe(tmpdir());
|
|
|
|
});
|
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
it('write report file', () => {
|
|
|
|
const content = randomUUID();
|
|
|
|
const reportPath = writeDumpReport('test', content);
|
|
|
|
expect(reportPath).toBeTruthy();
|
2024-11-05 11:49:21 +08:00
|
|
|
const reportContent = readFileSync(reportPath!, 'utf-8');
|
2024-08-15 17:59:43 +08:00
|
|
|
expect(reportContent).contains(content);
|
|
|
|
});
|
|
|
|
|
2024-08-31 08:17:50 +08:00
|
|
|
it('write report file with empty dump', () => {
|
|
|
|
const reportPath = writeDumpReport('test', []);
|
|
|
|
expect(reportPath).toBeTruthy();
|
2024-11-05 11:49:21 +08:00
|
|
|
const reportContent = readFileSync(reportPath!, 'utf-8');
|
2024-08-31 08:17:50 +08:00
|
|
|
expect(reportContent).contains('type="midscene_web_dump"');
|
|
|
|
});
|
|
|
|
|
2024-08-15 17:59:43 +08:00
|
|
|
it('write report file with attributes', () => {
|
|
|
|
const content = randomUUID();
|
|
|
|
const reportPath = writeDumpReport('test', [
|
|
|
|
{
|
|
|
|
dumpString: content,
|
|
|
|
attributes: {
|
|
|
|
foo: 'bar',
|
|
|
|
hello: 'world',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
expect(reportPath).toBeTruthy();
|
2024-11-05 11:49:21 +08:00
|
|
|
const reportContent = readFileSync(reportPath!, 'utf-8');
|
2024-08-15 17:59:43 +08:00
|
|
|
expect(reportContent).contains(content);
|
|
|
|
expect(reportContent).contains('foo="bar"');
|
|
|
|
expect(reportContent).contains('hello="world"');
|
|
|
|
});
|
|
|
|
|
2024-07-23 16:25:11 +08:00
|
|
|
it('overlapped', () => {
|
|
|
|
const container = { left: 100, top: 100, width: 100, height: 100 };
|
|
|
|
const target = { left: 150, top: 150, width: 100, height: 100 };
|
|
|
|
expect(overlapped(container, target)).toBeTruthy();
|
|
|
|
|
|
|
|
const target2 = { left: 200, top: 200, width: 100, height: 100 };
|
|
|
|
expect(overlapped(container, target2)).toBeFalsy();
|
|
|
|
});
|
2024-11-05 11:49:21 +08:00
|
|
|
|
|
|
|
it('reportHTMLContent', () => {
|
|
|
|
const reportA = reportHTMLContent([]);
|
|
|
|
expect(reportA).toContain(
|
|
|
|
'<script type="midscene_web_dump" type="application/json"></script>',
|
|
|
|
);
|
|
|
|
|
|
|
|
const content = randomUUID();
|
|
|
|
const reportB = reportHTMLContent(content);
|
|
|
|
expect(reportB).toContain(
|
2025-04-01 15:03:42 +08:00
|
|
|
`<script type="midscene_web_dump" type="application/json">\n${content}\n</script>`,
|
2024-11-05 11:49:21 +08:00
|
|
|
);
|
|
|
|
});
|
2024-08-04 08:28:19 +08:00
|
|
|
});
|
2024-09-29 17:16:07 +08:00
|
|
|
|
|
|
|
describe('extractJSONFromCodeBlock', () => {
|
|
|
|
it('should extract JSON from a direct JSON object', () => {
|
|
|
|
const input = '{ "key": "value" }';
|
|
|
|
const result = extractJSONFromCodeBlock(input);
|
|
|
|
expect(result).toBe('{ "key": "value" }');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should extract JSON from a code block with json language specifier', () => {
|
|
|
|
const input = '```json\n{ "key": "value" }\n```';
|
|
|
|
const result = extractJSONFromCodeBlock(input);
|
|
|
|
expect(result).toBe('{ "key": "value" }');
|
2024-12-20 15:18:52 +08:00
|
|
|
|
|
|
|
const input2 = ' ```JSON\n{ "key": "value" }\n```';
|
|
|
|
const result2 = extractJSONFromCodeBlock(input2);
|
|
|
|
expect(result2).toBe('{ "key": "value" }');
|
2024-09-29 17:16:07 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should extract JSON from a code block without language specifier', () => {
|
|
|
|
const input = '```\n{ "key": "value" }\n```';
|
|
|
|
const result = extractJSONFromCodeBlock(input);
|
|
|
|
expect(result).toBe('{ "key": "value" }');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should extract JSON-like structure from text', () => {
|
|
|
|
const input = 'Some text { "key": "value" } more text';
|
|
|
|
const result = extractJSONFromCodeBlock(input);
|
|
|
|
expect(result).toBe('{ "key": "value" }');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should return the original response if no JSON structure is found', () => {
|
|
|
|
const input = 'This is just plain text';
|
|
|
|
const result = extractJSONFromCodeBlock(input);
|
|
|
|
expect(result).toBe('This is just plain text');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle multi-line JSON objects', () => {
|
|
|
|
const input = `{
|
|
|
|
"key1": "value1",
|
|
|
|
"key2": {
|
|
|
|
"nestedKey": "nestedValue"
|
|
|
|
}
|
|
|
|
}`;
|
|
|
|
const result = extractJSONFromCodeBlock(input);
|
|
|
|
expect(result).toBe(input);
|
|
|
|
});
|
2025-01-02 21:23:30 +08:00
|
|
|
|
|
|
|
it('should handle JSON with point coordinates', () => {
|
|
|
|
const input = '(123,456)';
|
|
|
|
const result = safeParseJson(input);
|
|
|
|
expect(result).toEqual([123, 456]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse valid JSON string using JSON.parse', () => {
|
|
|
|
const input = '{"key": "value"}';
|
|
|
|
const result = safeParseJson(input);
|
|
|
|
expect(result).toEqual({ key: 'value' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse dirty JSON using dirty-json parser', () => {
|
|
|
|
const input = "{key: 'value'}"; // Invalid JSON but valid dirty-json
|
|
|
|
const result = safeParseJson(input);
|
|
|
|
expect(result).toEqual({ key: 'value' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw error for unparseable content', () => {
|
|
|
|
const input = 'not a json at all';
|
|
|
|
const result = safeParseJson(input);
|
|
|
|
expect(result).toEqual(input);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse JSON from code block', () => {
|
|
|
|
const input = '```json\n{"key": "value"}\n```';
|
|
|
|
const result = safeParseJson(input);
|
|
|
|
expect(result).toEqual({ key: 'value' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse complex nested JSON', () => {
|
|
|
|
const input = `{
|
|
|
|
"string": "value",
|
|
|
|
"number": 123,
|
|
|
|
"boolean": true,
|
|
|
|
"array": [1, 2, 3],
|
|
|
|
"object": {
|
|
|
|
"nested": "value"
|
|
|
|
}
|
|
|
|
}`;
|
|
|
|
const result = safeParseJson(input);
|
|
|
|
expect(result).toEqual({
|
|
|
|
string: 'value',
|
|
|
|
number: 123,
|
|
|
|
boolean: true,
|
|
|
|
array: [1, 2, 3],
|
|
|
|
object: {
|
|
|
|
nested: 'value',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
2024-09-29 17:16:07 +08:00
|
|
|
});
|
2025-03-17 19:19:54 +08:00
|
|
|
|
|
|
|
describe('qwen-vl', () => {
|
|
|
|
it('adaptQwenBbox', () => {
|
|
|
|
const result = adaptQwenBbox([100.3, 200.4, 301, 401]);
|
|
|
|
expect(result).toEqual([100, 200, 301, 401]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('adaptQwenBbox with 2 points', () => {
|
|
|
|
const result = adaptQwenBbox([100, 200]);
|
|
|
|
expect(result).toEqual([100, 200, 120, 220]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('adaptQwenBbox with invalid bbox data', () => {
|
|
|
|
expect(() => adaptQwenBbox([100])).toThrow();
|
|
|
|
});
|
2025-03-24 09:50:27 +08:00
|
|
|
|
|
|
|
it.skipIf(vlLocateMode() !== 'qwen-vl')('adaptBboxToRect', () => {
|
|
|
|
const result = adaptBboxToRect([100, 200, 300, 400], 400, 900, 30, 60);
|
|
|
|
expect(result).toMatchInlineSnapshot(`
|
|
|
|
{
|
|
|
|
"height": 200,
|
|
|
|
"left": 130,
|
|
|
|
"top": 260,
|
|
|
|
"width": 200,
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
});
|
2025-03-17 19:19:54 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('doubao-vision', () => {
|
|
|
|
it('preprocessDoubaoBboxJson', () => {
|
|
|
|
const input = '123 456';
|
|
|
|
const result = preprocessDoubaoBboxJson(input);
|
|
|
|
expect(result).toBe('123,456');
|
|
|
|
|
|
|
|
const input2 = '1 4';
|
|
|
|
const result2 = preprocessDoubaoBboxJson(input2);
|
|
|
|
expect(result2).toBe('1,4');
|
|
|
|
|
|
|
|
const input3 = '123 456\n789 100';
|
|
|
|
const result3 = preprocessDoubaoBboxJson(input3);
|
|
|
|
expect(result3).toBe('123,456\n789,100');
|
|
|
|
|
|
|
|
const input4 = '[123 456,789 100]';
|
|
|
|
const result4 = preprocessDoubaoBboxJson(input4);
|
|
|
|
expect(result4).toBe('[123,456,789,100]');
|
|
|
|
});
|
|
|
|
|
2025-03-24 09:50:27 +08:00
|
|
|
it('adaptDoubaoBbox with 2 points', () => {
|
|
|
|
const result = adaptDoubaoBbox([100, 200], 1000, 2000);
|
|
|
|
expect(result).toMatchInlineSnapshot(`
|
|
|
|
[
|
|
|
|
90,
|
|
|
|
390,
|
|
|
|
110,
|
|
|
|
410,
|
|
|
|
]
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
2025-03-17 19:19:54 +08:00
|
|
|
it('adaptDoubaoBbox', () => {
|
|
|
|
const result = adaptDoubaoBbox([100, 200, 300, 400], 1000, 2000);
|
2025-03-24 09:50:27 +08:00
|
|
|
expect(result).toMatchInlineSnapshot(`
|
|
|
|
[
|
|
|
|
100,
|
|
|
|
400,
|
|
|
|
300,
|
|
|
|
800,
|
|
|
|
]
|
|
|
|
`);
|
2025-03-17 19:19:54 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
it('adaptDoubaoBbox with 6 points', () => {
|
|
|
|
const result2 = adaptDoubaoBbox([100, 200, 300, 400, 100, 200], 1000, 2000);
|
2025-03-24 09:50:27 +08:00
|
|
|
expect(result2).toMatchInlineSnapshot(`
|
|
|
|
[
|
|
|
|
90,
|
|
|
|
390,
|
|
|
|
110,
|
|
|
|
410,
|
|
|
|
]
|
|
|
|
`);
|
2025-03-17 19:19:54 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
it('adaptDoubaoBbox with 8 points', () => {
|
|
|
|
const result3 = adaptDoubaoBbox(
|
|
|
|
[100, 200, 300, 200, 300, 400, 100, 400],
|
|
|
|
1000,
|
|
|
|
2000,
|
|
|
|
);
|
2025-03-24 09:50:27 +08:00
|
|
|
expect(result3).toMatchInlineSnapshot(`
|
|
|
|
[
|
|
|
|
100,
|
|
|
|
400,
|
|
|
|
300,
|
|
|
|
800,
|
|
|
|
]
|
|
|
|
`);
|
2025-03-17 19:19:54 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
it('adaptDoubaoBbox with invalid bbox data', () => {
|
|
|
|
expect(() => adaptDoubaoBbox([100], 1000, 2000)).toThrow();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2025-04-02 19:26:56 +08:00
|
|
|
describe('search area', () => {
|
|
|
|
it('mergeRects', () => {
|
|
|
|
const result = mergeRects([
|
|
|
|
{ left: 10, top: 10, width: 10, height: 500 },
|
|
|
|
{ left: 100, top: 100, width: 100, height: 100 },
|
|
|
|
]);
|
|
|
|
expect(result).toMatchInlineSnapshot(`
|
|
|
|
{
|
|
|
|
"height": 500,
|
|
|
|
"left": 10,
|
|
|
|
"top": 10,
|
|
|
|
"width": 190,
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
2025-03-24 09:50:27 +08:00
|
|
|
it('expandSearchArea', () => {
|
|
|
|
const result = expandSearchArea(
|
|
|
|
{ left: 100, top: 100, width: 100, height: 100 },
|
|
|
|
{ width: 1000, height: 1000 },
|
|
|
|
);
|
|
|
|
expect(result).toMatchInlineSnapshot(`
|
|
|
|
{
|
2025-04-03 06:01:06 +00:00
|
|
|
"height": 300,
|
|
|
|
"left": 0,
|
|
|
|
"top": 0,
|
|
|
|
"width": 300,
|
2025-03-24 09:50:27 +08:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('expandSearchArea with a big rect', () => {
|
|
|
|
const result = expandSearchArea(
|
|
|
|
{ left: 100, top: 100, width: 500, height: 500 },
|
|
|
|
{ width: 1000, height: 1000 },
|
|
|
|
);
|
|
|
|
expect(result).toMatchInlineSnapshot(`
|
|
|
|
{
|
2025-04-03 06:01:06 +00:00
|
|
|
"height": 820,
|
|
|
|
"left": 0,
|
|
|
|
"top": 0,
|
|
|
|
"width": 820,
|
2025-03-24 09:50:27 +08:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('expandSearchArea with a right-most rect', () => {
|
|
|
|
const result = expandSearchArea(
|
|
|
|
{ left: 951, top: 800, width: 50, height: 50 },
|
|
|
|
{ width: 1000, height: 1000 },
|
|
|
|
);
|
|
|
|
expect(result).toMatchInlineSnapshot(`
|
|
|
|
{
|
2025-04-03 06:01:06 +00:00
|
|
|
"height": 300,
|
|
|
|
"left": 826,
|
|
|
|
"top": 675,
|
|
|
|
"width": 174,
|
2025-03-24 09:50:27 +08:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2025-03-17 19:19:54 +08:00
|
|
|
describe('env', () => {
|
|
|
|
it('getAIConfig', () => {
|
|
|
|
const result = getAIConfig('NEVER_EXIST_CONFIG' as any);
|
|
|
|
expect(result).toBeUndefined();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('overrideAIConfig', () => {
|
|
|
|
expect(() =>
|
|
|
|
overrideAIConfig({
|
|
|
|
MIDSCENE_CACHE: {
|
|
|
|
foo: 123,
|
|
|
|
} as any,
|
|
|
|
}),
|
|
|
|
).toThrow();
|
|
|
|
});
|
|
|
|
});
|