feat(whatsapp): QWEN primary AI backend, Hermes fallback, conversation history, vehicle persistence, demo prompts
- Add QWEN (qwen3.6) as primary AI backend with short system prompt - Hermes remains as fallback with 45s timeout - Increase QWEN timeout to 35s, max_tokens to 4000 - Add conversation history loading from whatsapp_messages (last 4 msgs) - Persist detected vehicle in whatsapp_sessions table - Add 'limpiar chat' / 'nuevo chat' / 'reset' commands to clear history - Fix CSS conflict: rename whatsapp chat-panel classes to wa-chat-panel - Fix JS ID conflicts with chat.js widget (waChatPanel, waChatMessages, etc.) - Improve no-stock response: conversational with alternatives - Split search_query by | for multi-part lookups - Add DEMO_PROMPTS.md and DEMO_PROMPTS_V2.md
This commit is contained in:
@@ -1,20 +1,16 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Nexus POS — Auth Guard', () => {
|
||||
test('unauthenticated user is redirected to login', async ({ browser }) => {
|
||||
// Create incognito context without localStorage
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto('/pos/sale');
|
||||
await expect(page).toHaveURL(/\/pos\/login/);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('login page is accessible without token', async ({ browser }) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
test('unauthenticated user redirected to login', async ({ page }) => {
|
||||
// Ensure no auth state
|
||||
await page.goto('/pos/login');
|
||||
await expect(page.locator('input[type="password"], #password, input[name="pin"]')).toBeVisible();
|
||||
await context.close();
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
await page.goto('/pos/sale');
|
||||
// app-init.js redirects to /pos/login when no token is found
|
||||
await expect(page).toHaveURL(/login/i, { timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,88 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Nexus POS — Inventory', () => {
|
||||
test('inventory page loads with table or grid', async ({ page }) => {
|
||||
await page.goto('/pos/inventory');
|
||||
await expect(page.locator('#inventoryTable, .data-table, #partsGrid, .grid, table')).toBeVisible({ timeout: 10000 });
|
||||
const content = await page.locator('body').textContent();
|
||||
expect(content).toMatch(/inventario|stock|producto|parte/i);
|
||||
const FAKE_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksIm5hbWUiOiJUZXN0IFVzZXIifQ.signature';
|
||||
|
||||
async function setupAuth(page) {
|
||||
await page.goto('/pos/login');
|
||||
await page.evaluate((token) => {
|
||||
localStorage.setItem('pos_token', token);
|
||||
localStorage.setItem('pos_tenant_id', '11');
|
||||
}, FAKE_TOKEN);
|
||||
}
|
||||
|
||||
async function mockInventoryAPIs(page) {
|
||||
await page.route(/\/pos\/api\/inventory\/items\?page=.*&per_page=.*/, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
barcode: '123456789',
|
||||
part_number: 'TEST-001',
|
||||
name: 'Producto de prueba',
|
||||
brand: 'TestBrand',
|
||||
stock: 10,
|
||||
cost: 50.0,
|
||||
price_1: 100.0,
|
||||
price_2: 90.0,
|
||||
price_3: 80.0,
|
||||
location: 'A-1',
|
||||
},
|
||||
],
|
||||
pagination: { page: 1, total_pages: 1, total: 1 },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test('product detail modal or panel opens', async ({ page }) => {
|
||||
await page.route(/\/pos\/api\/inventory\/items\/\d+/, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
id: 1,
|
||||
barcode: '123456789',
|
||||
part_number: 'TEST-001',
|
||||
name: 'Producto de prueba',
|
||||
brand: 'TestBrand',
|
||||
location: 'A-1',
|
||||
stock: 10,
|
||||
cost: 50.0,
|
||||
price_1: 100.0,
|
||||
price_2: 90.0,
|
||||
price_3: 80.0,
|
||||
history: [],
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('Nexus POS — Inventory', () => {
|
||||
test('inventory page loads with table', async ({ page }) => {
|
||||
await setupAuth(page);
|
||||
await mockInventoryAPIs(page);
|
||||
await page.goto('/pos/inventory');
|
||||
// Try clicking first row or card
|
||||
const firstRow = page.locator('.data-table tbody tr, .grid .card, .inventory-row').first();
|
||||
await firstRow.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {});
|
||||
if (await firstRow.isVisible().catch(() => false)) {
|
||||
await firstRow.click();
|
||||
await expect(page.locator('.modal, .detail-panel, #detailPanel, [role="dialog"]')).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
await expect(page).toHaveTitle(/Inventario/i);
|
||||
await expect(page.locator('#stockTable')).toBeVisible({ timeout: 5000 });
|
||||
// Wait for virtual-scroll rows to render
|
||||
await page.waitForSelector('#productTableBody tr', { timeout: 5000 });
|
||||
const rows = page.locator('#productTableBody tr');
|
||||
await expect(rows.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('product detail modal opens', async ({ page }) => {
|
||||
await setupAuth(page);
|
||||
await mockInventoryAPIs(page);
|
||||
await page.goto('/pos/inventory');
|
||||
|
||||
await page.waitForSelector('#productTableBody tr', { timeout: 5000 });
|
||||
const firstRow = page.locator('#productTableBody tr').first();
|
||||
await firstRow.click();
|
||||
|
||||
// The detail view opens inside #historyModal
|
||||
await expect(page.locator('#historyModal')).toHaveClass(/is-open/, { timeout: 5000 });
|
||||
await expect(page.locator('#historyContent')).toContainText('Producto de prueba');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,71 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Nexus POS — Checkout', () => {
|
||||
test('POS sale page loads with cart', async ({ page }) => {
|
||||
await page.goto('/pos/sale');
|
||||
await expect(page.locator('#cartBody, .cart, #cartTable, .pos-cart')).toBeVisible({ timeout: 10000 });
|
||||
const content = await page.locator('body').textContent();
|
||||
expect(content).toMatch(/venta|carrito|total|pagar/i);
|
||||
const FAKE_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksIm5hbWUiOiJUZXN0IFVzZXIifQ.signature';
|
||||
|
||||
async function setupAuth(page) {
|
||||
await page.goto('/pos/login');
|
||||
await page.evaluate((token) => {
|
||||
localStorage.setItem('pos_token', token);
|
||||
localStorage.setItem('pos_tenant_id', '11');
|
||||
}, FAKE_TOKEN);
|
||||
}
|
||||
|
||||
async function mockPOSAPIs(page) {
|
||||
await page.route('/pos/api/register/current', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ register: { register_number: 1 } }),
|
||||
});
|
||||
});
|
||||
|
||||
test('catalog search from POS shows results', async ({ page }) => {
|
||||
await page.route(/\/pos\/api\/inventory\/items\?q=.*&per_page=.*/, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
part_number: 'TEST-001',
|
||||
name: 'Producto de prueba',
|
||||
brand: 'TestBrand',
|
||||
stock: 10,
|
||||
price_1: 100.0,
|
||||
price_2: 90.0,
|
||||
price_3: 80.0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('Nexus POS — Checkout', () => {
|
||||
test('POS page loads with cart', async ({ page }) => {
|
||||
await setupAuth(page);
|
||||
await mockPOSAPIs(page);
|
||||
await page.goto('/pos/sale');
|
||||
const searchInput = page.locator('#productSearch, #searchInput, input[placeholder*="buscar" i]').first();
|
||||
await expect(searchInput).toBeVisible({ timeout: 10000 });
|
||||
await searchInput.fill('freno');
|
||||
|
||||
await expect(page).toHaveTitle(/Nexus Autoparts/i);
|
||||
await expect(page.locator('#cartItems')).toBeVisible();
|
||||
await expect(page.locator('#cartBody')).toBeVisible();
|
||||
await expect(page.locator('#btnCobrar')).toBeVisible();
|
||||
});
|
||||
|
||||
test('catalog search from POS', async ({ page }) => {
|
||||
await setupAuth(page);
|
||||
await mockPOSAPIs(page);
|
||||
await page.goto('/pos/sale');
|
||||
|
||||
const searchInput = page.locator('#itemSearch');
|
||||
await expect(searchInput).toBeVisible();
|
||||
await searchInput.fill('test');
|
||||
await searchInput.press('Enter');
|
||||
await page.waitForTimeout(800);
|
||||
const hasDropdown = await page.locator('.search-dropdown, #searchDropdown, .parts-grid').first().isVisible().catch(() => false);
|
||||
expect(hasDropdown || true).toBe(true);
|
||||
|
||||
// Assert search results dropdown/grid appears
|
||||
const results = page.locator('#searchResults');
|
||||
await expect(results).toBeVisible({ timeout: 5000 });
|
||||
await expect(results).toContainText('Producto de prueba');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user