fix: refine scraper output behavior
This commit is contained in:
@@ -87,6 +87,12 @@ function parseEbayPrice(
|
||||
cleaned.toUpperCase().includes("US $")
|
||||
) {
|
||||
currency = "USD";
|
||||
} else if (cleaned.includes("£")) {
|
||||
currency = "GBP";
|
||||
} else if (cleaned.includes("€")) {
|
||||
currency = "EUR";
|
||||
} else if (cleaned.includes("¥")) {
|
||||
currency = "JPY";
|
||||
}
|
||||
|
||||
return { cents, currency };
|
||||
|
||||
@@ -1180,13 +1180,13 @@ export default async function fetchFacebookItems(
|
||||
|
||||
console.log(`\nFound ${ads.length} raw ads. Processing...`);
|
||||
|
||||
const progressBar = new cliProgress.SingleBar(
|
||||
{},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
const isTTY = process.stdout?.isTTY ?? false;
|
||||
const progressBar = isTTY
|
||||
? new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic)
|
||||
: null;
|
||||
const totalProgress = ads.length;
|
||||
const currentProgress = 0;
|
||||
progressBar.start(totalProgress, currentProgress);
|
||||
progressBar?.start(totalProgress, currentProgress);
|
||||
|
||||
const items = parseFacebookAds(ads);
|
||||
|
||||
@@ -1196,8 +1196,8 @@ export default async function fetchFacebookItems(
|
||||
typeof item.listingPrice?.cents === "number" && item.listingPrice.cents >= 0,
|
||||
);
|
||||
|
||||
progressBar.update(totalProgress);
|
||||
progressBar.stop();
|
||||
progressBar?.update(totalProgress);
|
||||
progressBar?.stop();
|
||||
|
||||
console.log(`\nParsed ${pricedItems.length} Facebook marketplace listings.`);
|
||||
return finalizeResults(pricedItems);
|
||||
|
||||
@@ -234,6 +234,68 @@ describe("eBay Scraper Cookie Handling", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("maps pound prices to GBP", async () => {
|
||||
global.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(`
|
||||
<html><body>
|
||||
<li class="s-item">
|
||||
<a href="/itm/123"></a>
|
||||
<h3>Stable Laptop Bundle</h3>
|
||||
<span class="s-item__price">£123.45</span>
|
||||
</li>
|
||||
</body></html>
|
||||
`),
|
||||
}),
|
||||
) as typeof fetch;
|
||||
|
||||
const results = await fetchEbayItems("laptop", 1000);
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
listingPrice: expect.objectContaining({ currency: "GBP", cents: 12345 }),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("maps euro and yen prices to the matching currency labels", async () => {
|
||||
global.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(`
|
||||
<html><body>
|
||||
<li class="s-item">
|
||||
<a href="/itm/123"></a>
|
||||
<h3>Euro Bundle</h3>
|
||||
<span class="s-item__price">€123.45</span>
|
||||
</li>
|
||||
<li class="s-item">
|
||||
<a href="/itm/456"></a>
|
||||
<h3>Yen Bundle</h3>
|
||||
<span class="s-item__price">¥123</span>
|
||||
</li>
|
||||
</body></html>
|
||||
`),
|
||||
}),
|
||||
) as typeof fetch;
|
||||
|
||||
const results = await fetchEbayItems("bundle", 1000, {
|
||||
keywords: ["bundle"],
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
listingPrice: expect.objectContaining({ currency: "EUR", cents: 12345 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
listingPrice: expect.objectContaining({ currency: "JPY", cents: 12300 }),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("prefers the discounted Canadian-formatted price", async () => {
|
||||
global.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import cliProgress from "cli-progress";
|
||||
import {
|
||||
classifyFacebookResponse,
|
||||
type FacebookListingDetails,
|
||||
@@ -16,6 +17,8 @@ import type { UnstableListingBuckets } from "../src/types/common";
|
||||
import { formatCookiesForHeader } from "../src/utils/cookies";
|
||||
import { formatCentsToCurrency } from "../src/utils/format";
|
||||
|
||||
const originalStdoutIsTTY = process.stdout.isTTY;
|
||||
|
||||
type Assert<T extends true> = T;
|
||||
type IsExact<T, U> =
|
||||
(<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2
|
||||
@@ -50,6 +53,7 @@ describe("Facebook Marketplace Scraper Core Tests", () => {
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
process.stdout.isTTY = originalStdoutIsTTY;
|
||||
});
|
||||
|
||||
describe("Cookie Parsing", () => {
|
||||
@@ -621,6 +625,68 @@ describe("Facebook Marketplace Scraper Core Tests", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("does not start a progress bar when stdout is not a TTY", async () => {
|
||||
const mockSearchHtml = `<html><body><script>"XCometMarketplaceSearchController"</script><script>${JSON.stringify({
|
||||
payload: {
|
||||
resultGroups: [
|
||||
{
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
listing: {
|
||||
id: "1",
|
||||
marketplace_listing_title: "Chair Listing",
|
||||
listing_price: {
|
||||
amount: "120.00",
|
||||
formatted_amount: "CA$120",
|
||||
currency: "CAD",
|
||||
},
|
||||
is_live: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})}</script></body></html>`;
|
||||
|
||||
process.stdout.isTTY = false;
|
||||
const startSpy = mock(() => {});
|
||||
const updateSpy = mock(() => {});
|
||||
const stopSpy = mock(() => {});
|
||||
const originalStart = cliProgress.SingleBar.prototype.start;
|
||||
const originalUpdate = cliProgress.SingleBar.prototype.update;
|
||||
const originalStop = cliProgress.SingleBar.prototype.stop;
|
||||
try {
|
||||
cliProgress.SingleBar.prototype.start = startSpy;
|
||||
cliProgress.SingleBar.prototype.update = updateSpy;
|
||||
cliProgress.SingleBar.prototype.stop = stopSpy;
|
||||
|
||||
global.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockSearchHtml),
|
||||
url: "https://www.facebook.com/marketplace/toronto/search?query=chair",
|
||||
headers: {
|
||||
get: () => null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await fetchFacebookItems("chair", 1, "toronto", 25);
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(startSpy).not.toHaveBeenCalled();
|
||||
expect(updateSpy).not.toHaveBeenCalled();
|
||||
expect(stopSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
cliProgress.SingleBar.prototype.start = originalStart;
|
||||
cliProgress.SingleBar.prototype.update = originalUpdate;
|
||||
cliProgress.SingleBar.prototype.stop = originalStop;
|
||||
}
|
||||
});
|
||||
|
||||
test("returns results and unstableResults when unstable mode is enabled", async () => {
|
||||
const mockSearchHtml = `<html><body><script>"XCometMarketplaceSearchController"</script><script>${JSON.stringify({
|
||||
payload: {
|
||||
|
||||
Reference in New Issue
Block a user