messages implemented

This commit is contained in:
2026-02-26 21:12:32 +01:00
parent d0aefc5ddc
commit 15b7b77d20
168 changed files with 14728 additions and 6786 deletions

View File

@@ -3,5 +3,5 @@ import { test, expect } from '@playwright/test';
test('home page loads and shows legacy page container', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Skinbase/i);
await expect(page.locator('.legacy-page')).toBeVisible();
await expect(page.locator('#homepage-root')).toBeVisible();
});

112
tests/e2e/messaging.spec.ts Normal file
View File

@@ -0,0 +1,112 @@
import { test, expect } from '@playwright/test'
import { execFileSync } from 'node:child_process'
type Fixture = {
email: string
password: string
conversation_id: number
}
function seedMessagingFixture(): Fixture {
const token = `${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`
const ownerEmail = `e2e-messages-owner-${token}@example.test`
const peerEmail = `e2e-messages-peer-${token}@example.test`
const ownerUsername = `e2eo${token}`.slice(0, 20)
const peerUsername = `e2ep${token}`.slice(0, 20)
const script = [
"use App\\Models\\User;",
"use App\\Models\\Conversation;",
"use App\\Models\\ConversationParticipant;",
"use App\\Models\\Message;",
"use Illuminate\\Support\\Facades\\Hash;",
"use Illuminate\\Support\\Carbon;",
`$owner = User::updateOrCreate(['email' => '${ownerEmail}'], [`,
" 'name' => 'E2E Owner',",
` 'username' => '${ownerUsername}',`,
" 'onboarding_step' => 'complete',",
" 'email_verified_at' => now(),",
" 'is_active' => 1,",
" 'password' => Hash::make('password'),",
"]);",
`$peer = User::updateOrCreate(['email' => '${peerEmail}'], [`,
" 'name' => 'E2E Peer',",
` 'username' => '${peerUsername}',`,
" 'onboarding_step' => 'complete',",
" 'email_verified_at' => now(),",
" 'is_active' => 1,",
" 'password' => Hash::make('password'),",
"]);",
"$conversation = Conversation::create(['type' => 'direct', 'created_by' => $owner->id]);",
"ConversationParticipant::insert([",
" ['conversation_id' => $conversation->id, 'user_id' => $owner->id, 'role' => 'admin', 'joined_at' => now(), 'last_read_at' => null],",
" ['conversation_id' => $conversation->id, 'user_id' => $peer->id, 'role' => 'member', 'joined_at' => now(), 'last_read_at' => now()],",
"]);",
"$first = Message::create(['conversation_id' => $conversation->id, 'sender_id' => $peer->id, 'body' => 'Seed hello']);",
"$last = Message::create(['conversation_id' => $conversation->id, 'sender_id' => $owner->id, 'body' => 'Seed latest from owner']);",
"$conversation->update(['last_message_at' => $last->created_at]);",
"ConversationParticipant::where('conversation_id', $conversation->id)->where('user_id', $peer->id)->update(['last_read_at' => Carbon::parse($last->created_at)->addSeconds(15)]);",
"echo json_encode(['email' => $owner->email, 'password' => 'password', 'conversation_id' => $conversation->id]);",
].join(' ')
const raw = execFileSync('php', ['artisan', 'tinker', `--execute=${script}`], {
cwd: process.cwd(),
encoding: 'utf8',
})
const lines = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{') && line.endsWith('}'))
if (!jsonLine) {
throw new Error(`Unable to parse fixture JSON from tinker output: ${raw}`)
}
return JSON.parse(jsonLine) as Fixture
}
async function login(page: Parameters<typeof test>[0]['page'], fixture: Fixture) {
await page.goto('/login')
await page.locator('input[name="email"]').fill(fixture.email)
await page.locator('input[name="password"]').fill(fixture.password)
await page.getByRole('button', { name: 'Sign In' }).click()
await page.waitForURL(/\/dashboard/)
}
test.describe('Messaging UI', () => {
test.describe.configure({ mode: 'serial' })
let fixture: Fixture
test.beforeAll(() => {
fixture = seedMessagingFixture()
})
test('restores draft from localStorage on reload', async ({ page }) => {
await login(page, fixture)
await page.goto(`/messages/${fixture.conversation_id}`)
const textarea = page.locator('textarea[placeholder^="Write a message"]')
const draft = 'E2E draft should survive reload'
await textarea.fill(draft)
await expect(textarea).toHaveValue(draft)
await page.reload({ waitUntil: 'domcontentloaded' })
await expect(textarea).toHaveValue(draft)
const stored = await page.evaluate((key) => window.localStorage.getItem(key), `nova_draft_${fixture.conversation_id}`)
expect(stored).toBe(draft)
})
test('shows seen indicator on latest direct message from current user', async ({ page }) => {
await login(page, fixture)
await page.goto(`/messages/${fixture.conversation_id}`)
await expect(page.locator('text=Seed latest from owner')).toBeVisible()
await expect(page.locator('text=/^Seen\\s.+\\sago$/')).toBeVisible()
})
})

View File

@@ -67,13 +67,13 @@ const PUBLIC_ROUTES: RouteFixture[] = [
{ url: '/comments/latest', label: 'Latest comments (new)' },
{ url: '/comments/monthly', label: 'Monthly commentators (new)' },
{ url: '/downloads/today', label: 'Today downloads (new)' },
{ url: '/top-authors', label: 'Top authors (legacy)' },
{ url: '/top-authors', label: 'Top authors (legacy)', expectUrlContains: '/creators/top' },
{ url: '/top-favourites', label: 'Top favourites (legacy)' },
{ url: '/today-downloads', label: 'Today downloads (legacy)' },
{ url: '/today-in-history', label: 'Today in history' },
{ url: '/monthly-commentators',label: 'Monthly commentators (legacy)' },
{ url: '/latest-comments', label: 'Latest comments (legacy)' },
{ url: '/interviews', label: 'Interviews' },
{ url: '/interviews', label: 'Interviews', expectUrlContains: '/stories' },
{ url: '/chat', label: 'Chat' },
// ── Forum ────────────────────────────────────────────────────────────────
@@ -84,6 +84,21 @@ const PUBLIC_ROUTES: RouteFixture[] = [
{ url: '/wallpapers', label: 'Wallpapers root' },
{ url: '/skins', label: 'Skins root' },
// ── Discover ──────────────────────────────────────────────────────────────
{ url: '/discover/trending', label: 'Discover: Trending' },
{ url: '/discover/fresh', label: 'Discover: Fresh' },
{ url: '/discover/top-rated', label: 'Discover: Top Rated' },
{ url: '/discover/most-downloaded', label: 'Discover: Most Downloaded' },
{ url: '/discover/on-this-day', label: 'Discover: On This Day' },
// ── Creators ──────────────────────────────────────────────────────────────
{ url: '/creators/top', label: 'Creators: Top' },
{ url: '/creators/rising', label: 'Creators: Rising' },
{ url: '/stories', label: 'Creator Stories' },
// ── Tags ──────────────────────────────────────────────────────────────────
{ url: '/tags', label: 'Tags index' },
// ── Auth pages (guest-only, publicly accessible) ─────────────────────────
{ url: '/login', label: 'Login page' },
{ url: '/register', label: 'Register page' },
@@ -103,6 +118,7 @@ const AUTH_ROUTES: RouteFixture[] = [
{ url: '/mybuddies', label: 'My buddies', requiresAuth: true },
{ url: '/buddies', label: 'Buddies', requiresAuth: true },
{ url: '/manage', label: 'Manage', requiresAuth: true },
{ url: '/dashboard/awards', label: 'Dashboard awards', requiresAuth: true },
];
// Routes that should 404 (to ensure 404 handling is clean and doesn't 500)
@@ -304,7 +320,7 @@ test.describe('Landmark spot-checks', () => {
test('Home page — has gallery section', async ({ page }) => {
const probe = attachProbes(page);
await page.goto('/', { waitUntil: 'domcontentloaded' });
await expect(page.locator('[data-nova-gallery], .gallery-grid, .container_photo')).toBeVisible();
await expect(page.locator('#homepage-root section').first()).toBeVisible();
expectCleanProbe(probe);
});