Add per-store test_query for automated store testing

Each store can now have its own test_query (e.g., "logitech" for
electronics stores). The "Test All" button uses each store's
configured query instead of prompting — just click and watch.

- Add test_query column (migration 003)
- Add field to YAML sync, store forms, and route schema
- Set test_query in HG Spot and Links.hr configs
- Test All runs immediately using per-store queries
- Hover test result to see which query was used

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
mariosemes
2026-03-26 23:08:41 +01:00
parent b3647be434
commit cb71421d8d
9 changed files with 39 additions and 45 deletions

View File

@@ -8,8 +8,6 @@
// Test all state
let testRunning = $state(false);
let testQuery = $state('');
let showTestPrompt = $state(false);
let testResults = $state(new Map());
onMount(async () => {
@@ -52,15 +50,7 @@
setTimeout(() => syncMessage = '', 5000);
}
function startTestAll() {
testQuery = '';
testResults = new Map();
showTestPrompt = true;
}
async function runTestAll() {
if (!testQuery.trim()) return;
showTestPrompt = false;
testRunning = true;
testResults = new Map();
@@ -68,19 +58,21 @@
// Mark all as pending
for (const store of enabledStores) {
testResults.set(store.id, { status: 'pending' });
testResults.set(store.id, { status: 'pending', query: store.test_query || 'test' });
}
testResults = new Map(testResults);
// Test each store sequentially
// Test each store sequentially using its own test_query
for (const store of enabledStores) {
testResults.set(store.id, { status: 'testing' });
const query = store.test_query || 'test';
testResults.set(store.id, { status: 'testing', query });
testResults = new Map(testResults);
try {
const result = await testStore(store.id, testQuery.trim());
const result = await testStore(store.id, query);
testResults.set(store.id, {
status: result.success ? 'success' : 'error',
query,
resultCount: result.parsedProducts?.length || 0,
duration: result.duration,
error: result.error,
@@ -88,6 +80,7 @@
} catch (err) {
testResults.set(store.id, {
status: 'error',
query,
resultCount: 0,
duration: 0,
error: err.message || 'Request failed',
@@ -109,7 +102,7 @@
<div class="flex-shrink-0 border-b border-surface-border px-6 py-3 flex items-center justify-between">
<h1 class="text-sm font-semibold text-text-primary">Stores</h1>
<div class="flex gap-2">
<button onclick={startTestAll} class="btn-secondary text-xs py-1" title="Test all enabled stores" disabled={testRunning}>
<button onclick={runTestAll} class="btn-secondary text-xs py-1" title="Test all enabled stores using their configured test queries" disabled={testRunning}>
{testRunning ? 'Testing...' : 'Test All'}
</button>
<button onclick={handleSync} class="btn-secondary text-xs py-1" title="Reload from YAML files">
@@ -128,24 +121,6 @@
</div>
{/if}
<!-- Test All Prompt -->
{#if showTestPrompt}
<div class="mx-6 mt-3 card p-4">
<p class="text-xs text-text-secondary mb-2">Enter a search term to test all enabled stores:</p>
<form onsubmit={(e) => { e.preventDefault(); runTestAll(); }} class="flex gap-2">
<input
type="text"
bind:value={testQuery}
placeholder="e.g. logitech m"
autofocus
class="input-field flex-1"
/>
<button type="submit" class="btn-primary text-xs" disabled={!testQuery.trim()}>Run Tests</button>
<button type="button" onclick={() => showTestPrompt = false} class="btn-secondary text-xs">Cancel</button>
</form>
</div>
{/if}
<!-- Content -->
<div class="flex-1 overflow-y-auto">
{#if loading}
@@ -218,7 +193,8 @@
Testing...
</span>
{:else if test.status === 'success'}
<span class="inline-flex items-center gap-1.5 text-2xs font-medium text-emerald-400">
<span class="inline-flex items-center gap-1.5 text-2xs font-medium text-emerald-400"
title="Query: {test.query}">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>

View File

@@ -12,7 +12,7 @@
let form = $state({
name: '', base_url: '', search_url: '',
sel_container: '', sel_name: '', sel_price: '', sel_link: '', sel_image: '',
category_id: '', currency: 'EUR', rate_limit: 2, render_js: false,
category_id: '', currency: 'EUR', rate_limit: 2, render_js: false, test_query: '',
user_agent: '', proxy_url: '', headers_json: '',
});
@@ -24,7 +24,7 @@
sel_container: store.sel_container, sel_name: store.sel_name, sel_price: store.sel_price,
sel_link: store.sel_link, sel_image: store.sel_image || '',
category_id: store.category_id?.toString() || '', currency: store.currency,
rate_limit: store.rate_limit, render_js: !!store.render_js,
rate_limit: store.rate_limit, render_js: !!store.render_js, test_query: store.test_query || '',
user_agent: store.user_agent || '', headers_json: store.headers_json || '',
};
loading = false;
@@ -93,6 +93,11 @@
<label class="label">Currency</label>
<input type="text" bind:value={form.currency} class="input-field" />
</div>
<div>
<label class="label">Test Query</label>
<input type="text" bind:value={form.test_query} placeholder="logitech" class="input-field" />
<p class="text-2xs text-text-tertiary mt-1">Search term used when testing this store</p>
</div>
</div>
</section>

View File

@@ -10,7 +10,7 @@
let form = $state({
name: '', base_url: '', search_url: '',
sel_container: '', sel_name: '', sel_price: '', sel_link: '', sel_image: '',
category_id: '', currency: 'EUR', rate_limit: 2, render_js: false,
category_id: '', currency: 'EUR', rate_limit: 2, render_js: false, test_query: '',
user_agent: '', proxy_url: '', headers_json: '',
});
@@ -76,6 +76,11 @@
<label class="label">Currency</label>
<input type="text" bind:value={form.currency} placeholder="EUR" class="input-field" />
</div>
<div>
<label class="label">Test Query</label>
<input type="text" bind:value={form.test_query} placeholder="logitech" class="input-field" />
<p class="text-2xs text-text-tertiary mt-1">Search term used when testing this store</p>
</div>
</div>
</section>

View File

@@ -0,0 +1 @@
ALTER TABLE stores ADD COLUMN test_query TEXT DEFAULT 'test';

View File

@@ -8,6 +8,7 @@ export interface Store {
search_url: string;
enabled: number;
render_js: number;
test_query: string;
sel_container: string;
sel_name: string;
sel_price: string;
@@ -40,6 +41,7 @@ export interface CreateStoreInput {
sel_link: string;
sel_image?: string;
render_js?: boolean;
test_query?: string;
rate_limit?: number;
rate_window?: number;
proxy_url?: string;
@@ -120,12 +122,12 @@ export function createStore(input: CreateStoreInput): Store {
db.run(`
INSERT INTO stores (name, slug, base_url, search_url, sel_container, sel_name, sel_price, sel_link, sel_image,
render_js, rate_limit, rate_window, proxy_url, user_agent, headers_json, currency, category_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
render_js, test_query, rate_limit, rate_window, proxy_url, user_agent, headers_json, currency, category_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
input.name, slug, input.base_url, input.search_url,
input.sel_container, input.sel_name, input.sel_price, input.sel_link, input.sel_image || null,
input.render_js ? 1 : 0,
input.render_js ? 1 : 0, input.test_query || 'test',
input.rate_limit ?? 2, input.rate_window ?? 1000,
input.proxy_url || null, input.user_agent || null, input.headers_json || null,
input.currency || 'EUR', input.category_id || null,

View File

@@ -36,6 +36,7 @@ export const storeRoutes: FastifyPluginAsync = async (app) => {
sel_link: { type: 'string', minLength: 1 },
sel_image: { type: 'string' },
render_js: { type: 'boolean' },
test_query: { type: 'string' },
rate_limit: { type: 'number' },
rate_window: { type: 'number' },
proxy_url: { type: 'string' },

View File

@@ -9,6 +9,7 @@ export interface StoreFileConfig {
search_url: string;
enabled?: boolean;
render_js?: boolean;
test_query?: string;
category?: string;
currency?: string;
selectors: {
@@ -99,7 +100,7 @@ export function syncFromFiles(storesDir: string): { created: number; updated: nu
if (existing) {
db.run(`
UPDATE stores SET
name = ?, base_url = ?, search_url = ?, enabled = ?, render_js = ?,
name = ?, base_url = ?, search_url = ?, enabled = ?, render_js = ?, test_query = ?,
sel_container = ?, sel_name = ?, sel_price = ?, sel_link = ?, sel_image = ?,
rate_limit = ?, rate_window = ?, proxy_url = ?, user_agent = ?, headers_json = ?,
currency = ?, category_id = ?, updated_at = datetime('now')
@@ -107,7 +108,7 @@ export function syncFromFiles(storesDir: string): { created: number; updated: nu
`, [
config.name, config.base_url, config.search_url,
config.enabled === false ? 0 : 1,
config.render_js ? 1 : 0,
config.render_js ? 1 : 0, config.test_query || 'test',
config.selectors.container, config.selectors.name,
config.selectors.price, config.selectors.link,
config.selectors.image || null,
@@ -119,15 +120,15 @@ export function syncFromFiles(storesDir: string): { created: number; updated: nu
updated++;
} else {
db.run(`
INSERT INTO stores (name, slug, base_url, search_url, enabled, render_js,
INSERT INTO stores (name, slug, base_url, search_url, enabled, render_js, test_query,
sel_container, sel_name, sel_price, sel_link, sel_image,
rate_limit, rate_window, proxy_url, user_agent, headers_json,
currency, category_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
config.name, slug, config.base_url, config.search_url,
config.enabled === false ? 0 : 1,
config.render_js ? 1 : 0,
config.render_js ? 1 : 0, config.test_query || 'test',
config.selectors.container, config.selectors.name,
config.selectors.price, config.selectors.link,
config.selectors.image || null,
@@ -165,6 +166,7 @@ function storeToConfig(store: any, categoryName?: string): StoreFileConfig {
if (store.sel_image) config.selectors.image = store.sel_image;
if (store.enabled === 0) config.enabled = false;
if (store.render_js) config.render_js = true;
if (store.test_query && store.test_query !== 'test') config.test_query = store.test_query;
if (categoryName) config.category = categoryName;
if (store.currency && store.currency !== 'EUR') config.currency = store.currency;
if (store.rate_limit && store.rate_limit !== 2) config.rate_limit = store.rate_limit;

View File

@@ -4,6 +4,7 @@ search_url: https://www.hgspot.hr/pretraga?q={query}&page=0
category: Electronics
currency: EUR
render_js: true
test_query: logitech
rate_limit: 1
selectors:

View File

@@ -3,6 +3,7 @@ base_url: https://www.links.hr
search_url: https://www.links.hr/hr/search?q={query}
category: Electronics
currency: EUR
test_query: logitech
selectors:
container: ".card.mobile-card"