import { test, expect } from '@playwright/test'; test.beforeEach(async ({ page }) => { await page.goto('/'); await page.evaluate(async () => { indexedDB.deleteDatabase('ttrpg-manager'); localStorage.clear(); }); await page.reload(); }); test('encounter tip suggests and applies a deterministic balance fix', async ({ page }) => { // Sample campaign: 2 level-3 PCs + a "Goblin Ambush" with 3 goblins. await page.getByLabel('Settings').click(); await page.getByRole('button', { name: 'Load sample campaign' }).click(); await expect(page.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible(); // Open the seeded encounter in the combat tracker. await page.getByLabel('Primary').getByRole('link', { name: 'Combat' }).click(); await page.getByText('Goblin Ambush').click(); // The contextual balance tip is present (LLM off → deterministic path). const tip = page.getByTestId('encounter-tip'); await expect(tip).toBeVisible(); // Each combatant row has a "Dmg" button — use it to count combatants. const hpControls = page.getByRole('button', { name: 'Dmg' }); const before = await hpControls.count(); await tip.getByRole('button', { name: 'Suggest a fix' }).click(); await expect(tip.getByRole('button', { name: 'Add to encounter' })).toBeVisible(); await tip.getByRole('button', { name: 'Add to encounter' }).click(); // Suggestion closes and at least one combatant was added. await expect(tip.getByRole('button', { name: 'Add to encounter' })).toBeHidden(); await expect.poll(async () => hpControls.count()).toBeGreaterThan(before); }); test('encounter tip uses the AI path when a provider is configured', async ({ page }) => { // Stub the Anthropic endpoint with a canned structured response. await page.route('**/v1/messages', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ content: [{ type: 'text', text: JSON.stringify({ reasoning: 'An ettin raises the threat to match the party.', add: [{ name: 'Ettin', count: 1 }], targetDifficulty: 'Hard', }) }], }), }); }); await page.getByLabel('Settings').click(); await page.getByRole('button', { name: 'Load sample campaign' }).click(); await expect(page.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible(); // Enable the assistant with a dummy key (network is stubbed). await page.getByLabel('Settings').click(); await page.getByLabel('Enable AI assistant').check(); await page.getByLabel('API key', { exact: true }).fill('sk-stub'); await page.getByLabel('Primary').getByRole('link', { name: 'Combat' }).click(); await page.getByText('Goblin Ambush').click(); const tip = page.getByTestId('encounter-tip'); await tip.getByRole('button', { name: 'Suggest a fix (AI)' }).click(); // The canned AI reasoning + the "AI suggestion" label render. await expect(tip.getByText('AI suggestion')).toBeVisible(); await expect(tip.getByText('An ettin raises the threat to match the party.')).toBeVisible(); await expect(tip.getByText('1× Ettin')).toBeVisible(); });