diff --git a/e2e/player-view.spec.ts b/e2e/player-view.spec.ts
index 87a2f90..3196906 100644
--- a/e2e/player-view.spec.ts
+++ b/e2e/player-view.spec.ts
@@ -39,3 +39,19 @@ test('player view shows the party and hides enemy HP numbers', async ({ page })
await expect(page.getByText('Bandit')).toBeVisible();
await expect(page.getByText(/Healthy|Bloodied|Down/)).toBeVisible();
});
+
+test('handout: GM pushes a handout to the player view', async ({ page }) => {
+ await page.getByRole('button', { name: '+ New campaign' }).first().click();
+ await page.locator('input[data-autofocus]').fill('Handout Camp');
+ await page.getByRole('button', { name: 'Create' }).click();
+
+ await page.getByRole('button', { name: /Handout/ }).click();
+ await page.getByLabel('Handout title').fill('A Mysterious Letter');
+ await page.getByLabel('Handout text').fill('Meet me at midnight.');
+ await page.getByRole('button', { name: 'Show to players' }).click();
+
+ await page.getByRole('link', { name: 'Dashboard' }).click();
+ await page.getByRole('link', { name: 'Player View' }).click();
+ await expect(page.getByRole('heading', { name: 'A Mysterious Letter' })).toBeVisible();
+ await expect(page.getByText('Meet me at midnight.')).toBeVisible();
+});
diff --git a/src/app/RootLayout.tsx b/src/app/RootLayout.tsx
index 9631d90..7435725 100644
--- a/src/app/RootLayout.tsx
+++ b/src/app/RootLayout.tsx
@@ -9,6 +9,7 @@ import { RollTray } from '@/components/ui/RollTray';
import { Select } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { SessionControl } from '@/features/play/SessionControl';
+import { HandoutControl } from '@/features/play/HandoutControl';
import { useSessionBroadcaster } from '@/features/play/useSessionBroadcaster';
const NAV = [
@@ -98,6 +99,7 @@ export function RootLayout() {
})}
+ {activeCampaign &&
}