optimizations
This commit is contained in:
@@ -8,6 +8,16 @@ type Fixture = {
|
||||
latest_message_id: number
|
||||
}
|
||||
|
||||
type InsertedMessage = {
|
||||
id: number
|
||||
body: string
|
||||
}
|
||||
|
||||
type ConversationSeed = {
|
||||
conversation_id: number
|
||||
latest_message_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`
|
||||
@@ -68,7 +78,95 @@ function seedMessagingFixture(): Fixture {
|
||||
return JSON.parse(jsonLine) as Fixture
|
||||
}
|
||||
|
||||
function insertReconnectRecoveryMessage(conversationId: number): InsertedMessage {
|
||||
const token = `${Date.now()}-${Math.floor(Math.random() * 100000)}`
|
||||
const body = `Reconnect recovery ${token}`
|
||||
|
||||
const script = [
|
||||
"use App\\Models\\Conversation;",
|
||||
"use App\\Models\\ConversationParticipant;",
|
||||
"use App\\Models\\Message;",
|
||||
`$conversation = Conversation::query()->findOrFail(${conversationId});`,
|
||||
"$senderId = ConversationParticipant::query()->where('conversation_id', $conversation->id)->where('role', 'member')->value('user_id');",
|
||||
`$message = Message::create(['conversation_id' => $conversation->id, 'sender_id' => $senderId, 'body' => '${body}']);`,
|
||||
"$conversation->update(['last_message_id' => $message->id, 'last_message_at' => $message->created_at]);",
|
||||
"echo json_encode(['id' => $message->id, 'body' => $message->body]);",
|
||||
].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 inserted message JSON from tinker output: ${raw}`)
|
||||
}
|
||||
|
||||
return JSON.parse(jsonLine) as InsertedMessage
|
||||
}
|
||||
|
||||
function seedAdditionalConversation(conversationId: number): ConversationSeed {
|
||||
const script = [
|
||||
"use App\\Models\\Conversation;",
|
||||
"use App\\Models\\ConversationParticipant;",
|
||||
"use App\\Models\\Message;",
|
||||
"use Illuminate\\Support\\Carbon;",
|
||||
"use Illuminate\\Support\\Facades\\Cache;",
|
||||
`$original = Conversation::query()->findOrFail(${conversationId});`,
|
||||
"$ownerId = (int) $original->created_by;",
|
||||
"$peerId = (int) ConversationParticipant::query()->where('conversation_id', $original->id)->where('user_id', '!=', $ownerId)->value('user_id');",
|
||||
"$conversation = Conversation::create(['type' => 'direct', 'created_by' => $ownerId]);",
|
||||
"ConversationParticipant::insert([",
|
||||
" ['conversation_id' => $conversation->id, 'user_id' => $ownerId, 'role' => 'admin', 'joined_at' => now(), 'last_read_at' => null],",
|
||||
" ['conversation_id' => $conversation->id, 'user_id' => $peerId, 'role' => 'member', 'joined_at' => now(), 'last_read_at' => now()],",
|
||||
"]);",
|
||||
"$first = Message::create(['conversation_id' => $conversation->id, 'sender_id' => $peerId, 'body' => 'Seed hello']);",
|
||||
"$last = Message::create(['conversation_id' => $conversation->id, 'sender_id' => $ownerId, 'body' => 'Seed latest from owner']);",
|
||||
"$conversation->update(['last_message_id' => $last->id, 'last_message_at' => $last->created_at]);",
|
||||
"ConversationParticipant::where('conversation_id', $conversation->id)->where('user_id', $peerId)->update(['last_read_at' => Carbon::parse($last->created_at)->addSeconds(15)]);",
|
||||
"foreach ([$ownerId, $peerId] as $uid) {",
|
||||
" $versionKey = 'messages:conversations:version:' . $uid;",
|
||||
" Cache::add($versionKey, 1, now()->addDay());",
|
||||
" Cache::increment($versionKey);",
|
||||
"}",
|
||||
"echo json_encode(['conversation_id' => $conversation->id, 'latest_message_id' => $last->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 additional conversation JSON from tinker output: ${raw}`)
|
||||
}
|
||||
|
||||
return JSON.parse(jsonLine) as ConversationSeed
|
||||
}
|
||||
|
||||
async function login(page: Parameters<typeof test>[0]['page'], fixture: Fixture) {
|
||||
const baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://skinbase26.test'
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: 'e2e_bot_bypass',
|
||||
value: '1',
|
||||
url: baseUrl,
|
||||
},
|
||||
])
|
||||
|
||||
await page.goto('/login')
|
||||
await page.locator('input[name="email"]').fill(fixture.email)
|
||||
await page.locator('input[name="password"]').fill(fixture.password)
|
||||
@@ -76,6 +174,15 @@ async function login(page: Parameters<typeof test>[0]['page'], fixture: Fixture)
|
||||
await page.waitForURL(/\/dashboard/)
|
||||
}
|
||||
|
||||
async function waitForRealtimeConnection(page: Parameters<typeof test>[0]['page']) {
|
||||
await page.waitForFunction(() => {
|
||||
const echo = window.Echo
|
||||
const connection = echo?.connector?.pusher?.connection
|
||||
|
||||
return Boolean(echo && connection && typeof connection.emit === 'function')
|
||||
}, { timeout: 10000 })
|
||||
}
|
||||
|
||||
test.describe('Messaging UI', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
@@ -110,4 +217,59 @@ test.describe('Messaging UI', () => {
|
||||
await expect(page.locator(`#message-${fixture.latest_message_id}`)).toContainText('Seed latest from owner')
|
||||
await expect(page.locator('text=/^Seen\\s.+\\sago$/')).toBeVisible()
|
||||
})
|
||||
|
||||
test('reconnect recovery fetches missed messages through delta without duplicates', async ({ page }) => {
|
||||
await login(page, fixture)
|
||||
await page.goto(`/messages/${fixture.conversation_id}`)
|
||||
await waitForRealtimeConnection(page)
|
||||
|
||||
await expect(page.locator(`#message-${fixture.latest_message_id}`)).toContainText('Seed latest from owner')
|
||||
|
||||
const inserted = insertReconnectRecoveryMessage(fixture.conversation_id)
|
||||
|
||||
await page.evaluate(() => {
|
||||
const connection = window.Echo?.connector?.pusher?.connection
|
||||
connection?.emit?.('connected')
|
||||
})
|
||||
|
||||
await expect(page.locator(`#message-${inserted.id}`)).toContainText(inserted.body)
|
||||
await expect(page.locator(`#message-${inserted.id}`)).toHaveCount(1)
|
||||
|
||||
await page.evaluate(() => {
|
||||
const connection = window.Echo?.connector?.pusher?.connection
|
||||
connection?.emit?.('connected')
|
||||
})
|
||||
|
||||
await expect(page.locator(`#message-${inserted.id}`)).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('reconnect recovery keeps sidebar unread summary consistent', async ({ page }) => {
|
||||
await login(page, fixture)
|
||||
|
||||
await page.route(/\/api\/messages\/\d+\/read$/, (route) => route.abort())
|
||||
|
||||
const isolatedConversation = seedAdditionalConversation(fixture.conversation_id)
|
||||
await page.goto(`/messages/${isolatedConversation.conversation_id}`)
|
||||
|
||||
const unreadStat = page.getByTestId('messages-stat-unread')
|
||||
const unreadBefore = parseUnreadCount(await unreadStat.innerText())
|
||||
await waitForRealtimeConnection(page)
|
||||
|
||||
const inserted = insertReconnectRecoveryMessage(isolatedConversation.conversation_id)
|
||||
|
||||
await page.evaluate(() => {
|
||||
const connection = window.Echo?.connector?.pusher?.connection
|
||||
connection?.emit?.('connected')
|
||||
})
|
||||
|
||||
await expect(page.locator(`#message-${inserted.id}`)).toContainText(inserted.body)
|
||||
|
||||
const unreadAfter = parseUnreadCount(await unreadStat.innerText())
|
||||
expect(unreadAfter).toBeGreaterThan(unreadBefore)
|
||||
})
|
||||
})
|
||||
|
||||
function parseUnreadCount(text: string): number {
|
||||
const digits = (text.match(/\d+/g) ?? []).join('')
|
||||
return Number.parseInt(digits || '0', 10)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user