Force new git SHA to avoid CI scientific notation bug. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
167 lines
5.4 KiB
TypeScript
167 lines
5.4 KiB
TypeScript
import puppeteer from 'puppeteer';
|
|
|
|
async function sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function main() {
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
await page.setViewport({ width: 1920, height: 1080 });
|
|
|
|
// Go to a product detail page
|
|
await page.goto('https://shop.bestdispensary.com/brand/dime', {
|
|
waitUntil: 'networkidle2',
|
|
timeout: 60000
|
|
});
|
|
await sleep(3000);
|
|
|
|
// Bypass age gate
|
|
const ageGate = await page.$('[data-testid="age-gate-modal"]');
|
|
if (ageGate) {
|
|
const btn = await page.$('[data-testid="age-gate-submit-button"]');
|
|
if (btn) await btn.click();
|
|
await sleep(2000);
|
|
}
|
|
|
|
// Get first product URL
|
|
const productUrl = await page.evaluate(() => {
|
|
const a = document.querySelector('a[href*="/product/"]');
|
|
return a ? 'https://shop.bestdispensary.com' + a.getAttribute('href') : null;
|
|
});
|
|
|
|
if (!productUrl) {
|
|
console.log('No product found');
|
|
await browser.close();
|
|
return;
|
|
}
|
|
|
|
console.log('Checking product: ' + productUrl + '\n');
|
|
await page.goto(productUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
await sleep(2000);
|
|
|
|
// Look for inventory/stock info
|
|
const inventoryData = await page.evaluate(() => {
|
|
const data: any = {};
|
|
|
|
// Check for stock/inventory elements
|
|
const stockSelectors = [
|
|
'[class*="stock"]',
|
|
'[class*="Stock"]',
|
|
'[class*="inventory"]',
|
|
'[class*="Inventory"]',
|
|
'[class*="quantity"]',
|
|
'[class*="Quantity"]',
|
|
'[class*="available"]',
|
|
'[class*="Available"]',
|
|
'[class*="in-stock"]',
|
|
'[class*="out-of-stock"]',
|
|
'[data-stock]',
|
|
'[data-quantity]',
|
|
'[data-inventory]',
|
|
];
|
|
|
|
data.stockElements = [];
|
|
stockSelectors.forEach(sel => {
|
|
document.querySelectorAll(sel).forEach(el => {
|
|
data.stockElements.push({
|
|
selector: sel,
|
|
text: el.textContent?.trim().slice(0, 100),
|
|
dataAttrs: Object.keys((el as HTMLElement).dataset || {}),
|
|
});
|
|
});
|
|
});
|
|
|
|
// Check for "Add to cart" button state (disabled = out of stock)
|
|
const addToCartBtn = document.querySelector('button[class*="add"], button[class*="cart"]');
|
|
data.addToCartBtn = {
|
|
found: !!addToCartBtn,
|
|
disabled: (addToCartBtn as HTMLButtonElement)?.disabled,
|
|
text: addToCartBtn?.textContent?.trim(),
|
|
};
|
|
|
|
// Check page source for inventory keywords
|
|
const bodyText = document.body.innerText;
|
|
data.hasStockText = bodyText.includes('stock') || bodyText.includes('Stock');
|
|
data.hasInventoryText = bodyText.includes('inventory') || bodyText.includes('Inventory');
|
|
data.hasQuantityText = bodyText.includes('quantity') || bodyText.includes('Quantity');
|
|
data.hasAvailableText = bodyText.includes('available') || bodyText.includes('Available');
|
|
|
|
// Get all data attributes on the page
|
|
data.allDataAttrs = [];
|
|
document.querySelectorAll('[data-product-id], [data-sku], [data-variant]').forEach(el => {
|
|
const attrs: any = {};
|
|
Object.entries((el as HTMLElement).dataset).forEach(([k, v]) => {
|
|
attrs[k] = v;
|
|
});
|
|
if (Object.keys(attrs).length > 0) {
|
|
data.allDataAttrs.push(attrs);
|
|
}
|
|
});
|
|
|
|
// Check for JSON-LD or schema data
|
|
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
data.jsonLd = [];
|
|
scripts.forEach(s => {
|
|
try {
|
|
const json = JSON.parse(s.textContent || '');
|
|
data.jsonLd.push(json);
|
|
} catch {}
|
|
});
|
|
|
|
// Check Next.js data
|
|
const nextData = document.getElementById('__NEXT_DATA__');
|
|
if (nextData) {
|
|
try {
|
|
const json = JSON.parse(nextData.textContent || '');
|
|
data.hasNextData = true;
|
|
data.nextDataKeys = Object.keys(json);
|
|
// Look for product data in props
|
|
if (json.props?.pageProps?.product) {
|
|
data.productFromNext = json.props.pageProps.product;
|
|
}
|
|
if (json.props?.pageProps) {
|
|
data.pagePropsKeys = Object.keys(json.props.pageProps);
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
return data;
|
|
});
|
|
|
|
console.log('Inventory Analysis:\n');
|
|
console.log('Stock elements found: ' + inventoryData.stockElements.length);
|
|
inventoryData.stockElements.forEach((s: any) => {
|
|
console.log(' - ' + s.selector + ': "' + s.text + '"');
|
|
});
|
|
|
|
console.log('\nAdd to Cart button: ' + JSON.stringify(inventoryData.addToCartBtn));
|
|
|
|
console.log('\nText checks:');
|
|
console.log(' Has "stock": ' + inventoryData.hasStockText);
|
|
console.log(' Has "inventory": ' + inventoryData.hasInventoryText);
|
|
console.log(' Has "quantity": ' + inventoryData.hasQuantityText);
|
|
console.log(' Has "available": ' + inventoryData.hasAvailableText);
|
|
|
|
console.log('\nData attributes: ' + JSON.stringify(inventoryData.allDataAttrs));
|
|
console.log('\nJSON-LD: ' + JSON.stringify(inventoryData.jsonLd, null, 2));
|
|
|
|
if (inventoryData.hasNextData) {
|
|
console.log('\nNext.js data found!');
|
|
console.log(' Keys: ' + inventoryData.nextDataKeys);
|
|
console.log(' Page props keys: ' + inventoryData.pagePropsKeys);
|
|
if (inventoryData.productFromNext) {
|
|
console.log('\n Product data from Next.js:');
|
|
console.log(JSON.stringify(inventoryData.productFromNext, null, 2));
|
|
}
|
|
}
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
main().catch(console.error);
|