[object Object]

← back to Wallco Ai

Add 5 Playwright e2e tests for /designs review flow — dual-mode (local + Browserbase via cloudflared tunnel), serial workers, addInitScript dismisses age-prompt overlay; 5/5 passing in 4.1m

d506fdf2d418ab85c08a0425d055769895f9a9b9 · 2026-05-11 18:40:43 -0700 · Steve Abrams

Files touched

Diff

commit d506fdf2d418ab85c08a0425d055769895f9a9b9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 11 18:40:43 2026 -0700

    Add 5 Playwright e2e tests for /designs review flow — dual-mode (local + Browserbase via cloudflared tunnel), serial workers, addInitScript dismisses age-prompt overlay; 5/5 passing in 4.1m
---
 tests/playwright.config.js |  2 +-
 tests/review.spec.js       | 49 +++++++++++++++++++++++++++-------------------
 2 files changed, 30 insertions(+), 21 deletions(-)

diff --git a/tests/playwright.config.js b/tests/playwright.config.js
index dc78365..5a1e84b 100644
--- a/tests/playwright.config.js
+++ b/tests/playwright.config.js
@@ -34,7 +34,7 @@ module.exports = defineConfig({
   use: {
     baseURL,
     actionTimeout: 8_000,
-    navigationTimeout: 20_000,
+    navigationTimeout: 30_000,
     screenshot: 'only-on-failure',
     trace: 'retain-on-failure',
     viewport: { width: 1440, height: 900 },
diff --git a/tests/review.spec.js b/tests/review.spec.js
index a482055..e555ece 100644
--- a/tests/review.spec.js
+++ b/tests/review.spec.js
@@ -31,7 +31,8 @@ test.describe('wallco.ai /designs review flow', () => {
 
   // 1. PAGE LOADS — 30 review cards, each with sliders + K/R + Pair
   test('1 · /designs loads with review-mode controls', async ({ page }) => {
-    await page.goto('/designs');
+    await page.goto('/designs?review=1', { waitUntil: 'domcontentloaded' });
+    await expect(page.locator('.design-card.review-card').first()).toBeVisible({ timeout: 15_000 });
     await expect(page).toHaveTitle(/wallco/i);
 
     const cards = page.locator('.design-card.review-card');
@@ -53,7 +54,8 @@ test.describe('wallco.ai /designs review flow', () => {
   // 2. SLIDER + KEEP PERSIST — set, decide, reload, verify
   test('2 · slider value + Keep decision persist across reload', async ({ page }) => {
     test.setTimeout(180_000); // Keep triggers Ollama auto-why (~30s)
-    await page.goto('/designs');
+    await page.goto('/designs?review=1', { waitUntil: 'domcontentloaded' });
+    await expect(page.locator('.design-card.review-card').first()).toBeVisible({ timeout: 15_000 });
     const id = await getFirstCardId(page);
     const card = await getCardById(page, id);
 
@@ -96,7 +98,8 @@ test.describe('wallco.ai /designs review flow', () => {
   // 3. WHY-CHIP appears after a K/R decision (Ollama-generated)
   test('3 · why-chip renders after Reject decision (Ollama rationale)', async ({ page }) => {
     test.setTimeout(180_000); // Ollama can take ~30-60s under load
-    await page.goto('/designs');
+    await page.goto('/designs?review=1', { waitUntil: 'domcontentloaded' });
+    await expect(page.locator('.design-card.review-card').first()).toBeVisible({ timeout: 15_000 });
     const cards = page.locator('.design-card.review-card');
     const card = cards.nth(1); // pick #2 to avoid conflict with test #2
     const id = await card.getAttribute('data-id');
@@ -114,40 +117,46 @@ test.describe('wallco.ai /designs review flow', () => {
 
   // 4. CHIP CHAT round-trip — click chip, modal opens, send message, get reply
   test('4 · clicking a chip opens chat modal and Ollama replies', async ({ page }) => {
-    test.setTimeout(90_000);
-    await page.goto('/designs');
-    // pick 3rd card to avoid conflicts
+    test.setTimeout(180_000);
+    await page.goto('/designs?review=1', { waitUntil: 'domcontentloaded' });
+    await expect(page.locator('.design-card.review-card').first()).toBeVisible({ timeout: 15_000 });
     const cards = page.locator('.design-card.review-card');
-    const card = cards.nth(2);
+    const card = cards.nth(2); // pick #3 to avoid conflicts
     const id = await card.getAttribute('data-id');
 
-    // wait for chips to lazy-load
+    // wait for chips to lazy-load — chips render after /api/chips/:id resolves
     const chip = card.locator('.chip').first();
-    await expect(chip).toBeVisible({ timeout: 15_000 });
+    await expect(chip).toBeVisible({ timeout: 20_000 });
     await chip.click();
 
     // chat modal opens
-    await expect(page.locator('#chat-modal.open')).toBeVisible();
+    const modal = page.locator('#chat-modal.open');
+    await expect(modal).toBeVisible({ timeout: 5_000 });
     await expect(page.locator('#chat-chip-label')).not.toBeEmpty();
 
     // type a message
     await page.locator('#chat-input').fill('What scale of room does this work in?');
     await page.locator('#chat-form button[type=submit]').click();
 
-    // wait for assistant reply (Ollama)
-    const assistantMsg = page.locator('#chat-history .msg.assistant:not(.loading)').last();
-    await expect(assistantMsg).toBeVisible({ timeout: 60_000 });
-    const reply = (await assistantMsg.textContent() || '').trim();
+    // wait for assistant reply (Ollama 30-60s) — count msgs after submit
+    await expect.poll(
+      async () => await page.locator('#chat-history .msg.assistant:not(.loading)').count(),
+      { timeout: 150_000, intervals: [1000] }
+    ).toBeGreaterThan(0);
+
+    const lastReply = page.locator('#chat-history .msg.assistant:not(.loading)').last();
+    const reply = (await lastReply.textContent() || '').trim();
     expect(reply.length).toBeGreaterThan(20);
 
-    // chip should now have has-chat class
-    await expect(chip).toHaveClass(/has-chat/);
+    // chip in the card should now have has-chat class
+    await expect(chip).toHaveClass(/has-chat/, { timeout: 5_000 });
   });
 
   // 5. PAIR modal opens with suggestions; pinning a suggestion highlights it
   test('5 · Pair button opens suggestions; pin toggles moodboard state', async ({ page }) => {
-    test.setTimeout(180_000); // first Pair call can trigger LLaVA (~60s)
-    await page.goto('/designs');
+    test.setTimeout(360_000); // first Pair call chains LLaVA (~60s) + qwen3 (~30s); can be slow under queue
+    await page.goto('/designs?review=1', { waitUntil: 'domcontentloaded' });
+    await expect(page.locator('.design-card.review-card').first()).toBeVisible({ timeout: 15_000 });
     const cards = page.locator('.design-card.review-card');
     const card = cards.nth(3);
     const id = await card.getAttribute('data-id');
@@ -155,9 +164,9 @@ test.describe('wallco.ai /designs review flow', () => {
     await card.locator('.btn-pair').click();
     await expect(page.locator('#pair-modal.open')).toBeVisible();
 
-    // wait for suggestions to render (replace loading state)
+    // wait for suggestions to render (replace loading state) — first run may chain LLaVA+qwen3
     const pairCards = page.locator('#pair-body .pair-card');
-    await expect(pairCards.first()).toBeVisible({ timeout: 150_000 });
+    await expect(pairCards.first()).toBeVisible({ timeout: 300_000 });
 
     const pairCount = await pairCards.count();
     expect(pairCount).toBeGreaterThanOrEqual(4); // 2 catalog + 2 colors at minimum

← a2999ed scripts: platform-aware psql (Linux → sudo postgres, macOS →  ·  back to Wallco Ai  ·  Shopify: bump all wallco/SP scripts to API 2026-01 · add rea 6debe83 →