152 lines
5.5 KiB
JavaScript
152 lines
5.5 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
cleanItemName,
|
|
combineRects,
|
|
displayCaptureIssue,
|
|
isCalibrationReady,
|
|
isRectInside,
|
|
parseFieldValue,
|
|
parseStoreHeaderText,
|
|
parseTooltipText,
|
|
rectFromPoints,
|
|
resolveFieldRect,
|
|
} from "../src/parser.js";
|
|
import { isWhiteTextPixel } from "../src/vision.js";
|
|
|
|
test("keeps white text pixels and removes colored or dark pixels", () => {
|
|
assert.equal(isWhiteTextPixel(220, 215, 195), true);
|
|
assert.equal(isWhiteTextPixel(210, 165, 80), false);
|
|
assert.equal(isWhiteTextPixel(120, 120, 120), false);
|
|
});
|
|
|
|
test("keeps only letters, digits and spaces in OCR item names", () => {
|
|
assert.equal(cleanItemName(" ~~ Soulshot: S-grade ` "), "Soulshot S grade");
|
|
assert.equal(cleanItemName("Кристалл № 12"), "Кристалл 12");
|
|
});
|
|
|
|
test("explains why display capture is unavailable", () => {
|
|
assert.equal(displayCaptureIssue({ secure: true, supported: true, topLevel: true }), null);
|
|
assert.match(displayCaptureIssue({ secure: false, supported: false, topLevel: true }).detail, /HTTPS/);
|
|
assert.match(displayCaptureIssue({ secure: true, supported: true, topLevel: false }).title, /встроенном/);
|
|
});
|
|
|
|
test("normalizes a reverse drag into a positive rectangle", () => {
|
|
assert.deepEqual(rectFromPoints({ x: 140, y: 90 }, { x: 20, y: 30 }), {
|
|
x: 20,
|
|
y: 30,
|
|
width: 120,
|
|
height: 60,
|
|
});
|
|
});
|
|
|
|
test("moves calibrated fields together with the detected anchor", () => {
|
|
assert.deepEqual(
|
|
resolveFieldRect(
|
|
{ x: 400, y: 250 },
|
|
{ offsetX: 32, offsetY: 80, width: 180, height: 24 },
|
|
),
|
|
{ x: 432, y: 330, width: 180, height: 24 },
|
|
);
|
|
});
|
|
|
|
test("combines the calibrated name and price rows into one OCR window", () => {
|
|
assert.deepEqual(
|
|
combineRects(
|
|
{ x: 100, y: 40, width: 220, height: 18 },
|
|
{ x: 120, y: 90, width: 160, height: 18 },
|
|
),
|
|
{ x: 100, y: 40, width: 220, height: 68 },
|
|
);
|
|
});
|
|
|
|
test("parses formatted prices and rejects empty numeric OCR", () => {
|
|
assert.equal(parseFieldValue("price", "1 250,000 adena"), 1250000);
|
|
assert.equal(parseFieldValue("quantity", "not found"), null);
|
|
assert.equal(parseFieldValue("item", " Soulshot: S-grade\n"), "Soulshot: S-grade");
|
|
});
|
|
|
|
test("requires both anchors and exact OCR regions", () => {
|
|
assert.equal(isCalibrationReady({ saleAnchor: null }), false);
|
|
assert.equal(
|
|
isCalibrationReady({
|
|
saleAnchor: { image: "data:image/png;base64,x", width: 20, height: 10 },
|
|
storeHeaderRegion: { offsetX: -50, offsetY: -300, width: 250, height: 24 },
|
|
tooltipSearchRegion: { offsetX: -100, offsetY: -300, width: 300, height: 100 },
|
|
tooltipAnchor: { image: "data:image/png;base64,y", width: 30, height: 12 },
|
|
itemNameRegion: { offsetX: 0, offsetY: -18, width: 220, height: 18 },
|
|
itemPriceRegion: { offsetX: 0, offsetY: 0, width: 160, height: 18 },
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("detects OCR rectangles outside the captured frame", () => {
|
|
assert.equal(isRectInside({ x: 10, y: 10, width: 20, height: 20 }, 100, 100), true);
|
|
assert.equal(isRectInside({ x: 90, y: 10, width: 20, height: 20 }, 100, 100), false);
|
|
});
|
|
|
|
test("parses a hovered item tooltip", () => {
|
|
assert.deepEqual(
|
|
parseTooltipText("Tears of Eva\nPrice : 3,000,000 Adena\n(3 Million Adena)"),
|
|
{
|
|
name: "Tears of Eva",
|
|
quantity: 1,
|
|
priceAdena: 3000000,
|
|
rawText: "Tears of Eva\nPrice : 3,000,000 Adena\n(3 Million Adena)",
|
|
},
|
|
);
|
|
assert.deepEqual(parseTooltipText("Animal Skin (18)\nFor Each 400 Adena"), {
|
|
name: "Animal Skin",
|
|
quantity: 18,
|
|
priceAdena: 400,
|
|
rawText: "Animal Skin (18)\nFor Each 400 Adena",
|
|
});
|
|
assert.equal(parseTooltipText("Animal Skin (18)\n400")?.priceAdena, 400);
|
|
assert.equal(parseTooltipText("Animal Skin (18)\nPrice : 400")?.priceAdena, 400);
|
|
assert.equal(parseTooltipText("Blessed Spiritshot D (5,600)\nFor Each 56 Adena")?.quantity, 5600);
|
|
assert.deepEqual(parseTooltipText("Sword of Valhalla\nPrice :\nFor Each 56 Adena"), {
|
|
name: "Sword of Valhalla",
|
|
quantity: 1,
|
|
priceAdena: 56,
|
|
rawText: "Sword of Valhalla\nPrice :\nFor Each 56 Adena",
|
|
});
|
|
assert.deepEqual(parseTooltipText("Animal Skin (18)\nWeight 0\nPrice :\nFor Each 400 Adena"), {
|
|
name: "Animal Skin",
|
|
quantity: 18,
|
|
priceAdena: 400,
|
|
rawText: "Animal Skin (18)\nWeight 0\nPrice :\nFor Each 400 Adena",
|
|
});
|
|
assert.deepEqual(
|
|
parseTooltipText("Blessed Spiritshot: D-Grade (D) (5,600)\nFor Each 56 Adena"),
|
|
{
|
|
name: "Blessed Spiritshot D Grade D",
|
|
quantity: 5600,
|
|
priceAdena: 56,
|
|
rawText: "Blessed Spiritshot: D-Grade (D) (5,600)\nFor Each 56 Adena",
|
|
},
|
|
);
|
|
assert.deepEqual(parseTooltipText("~~ Animal Skin (18),,.\nFor Each 400 Adena"), {
|
|
name: "Animal Skin",
|
|
quantity: 18,
|
|
priceAdena: 400,
|
|
rawText: "~~ Animal Skin (18),,.\nFor Each 400 Adena",
|
|
});
|
|
assert.equal(parseTooltipText("Purchase List\nAdena 5,403,160"), null);
|
|
});
|
|
|
|
test("parses trade side and merchant from the store header", () => {
|
|
assert.deepEqual(parseStoreHeaderText("Private Store(Buy) - Hikvision"), {
|
|
side: "buy",
|
|
merchant: "Hikvision",
|
|
rawText: "Private Store(Buy) - Hikvision",
|
|
});
|
|
assert.deepEqual(parseStoreHeaderText(" Private Store({Sell) - Dwa \n"), {
|
|
side: "sell",
|
|
merchant: "Dwa",
|
|
rawText: "Private Store({Sell) - Dwa",
|
|
});
|
|
assert.equal(parseStoreHeaderText("Private Store(Buy) - Hik vision")?.merchant, "Hikvision");
|
|
assert.equal(parseStoreHeaderText("Items on Sale"), null);
|
|
});
|