feat: drag relation tags along threads

This commit is contained in:
2026-08-14 18:26:20 +02:00
parent 5d59af1dd9
commit b39d2efa75
11 changed files with 164 additions and 29 deletions
+21 -1
View File
@@ -1,5 +1,15 @@
import { expect, test, type Page } from '@playwright/test'
async function dragBy(page: Page, locator: ReturnType<Page['locator']>, deltaX: number, deltaY: number) {
const box = await locator.boundingBox()
if (!box) throw new Error('Drag target is not visible')
const start = { x: box.x + box.width / 2, y: box.y + Math.min(18, box.height / 4) }
await page.mouse.move(start.x, start.y)
await page.mouse.down()
await page.mouse.move(start.x + deltaX, start.y + deltaY, { steps: 8 })
await page.mouse.up()
}
async function waitForSave(page: Page, action: () => Promise<void>) {
const response = page.waitForResponse(candidate => candidate.request().method() === 'PUT' && candidate.url().includes('/api/levels/') && candidate.ok())
await action()
@@ -130,11 +140,18 @@ test('a cloned Glass Harbor level can be solved without modifying its template',
await expect(page.locator('.connections path')).toHaveCount(3)
const relationTag = page.locator('.thread-tag').filter({ hasText: 'Proof Elias is the driver' })
await expect(relationTag).toContainText('Proof Elias is the driver')
const tagBefore = await relationTag.boundingBox()
await waitForSave(page, () => dragBy(page, relationTag, 75, -18))
const tagAfter = await relationTag.boundingBox()
expect(tagBefore).not.toBeNull()
expect(tagAfter).not.toBeNull()
expect(Math.abs(tagAfter!.x - tagBefore!.x) + Math.abs(tagAfter!.y - tagBefore!.y)).toBeGreaterThan(20)
await relationTag.click()
await expect(relationTag).toHaveClass(/\bexpanded\b/)
await expect(relationTag).toHaveAttribute('aria-expanded', 'true')
await relationTag.click()
await expect(page.getByText('Edit red thread')).toBeVisible()
expect(await page.getByRole('slider', { name: 'Tag position', exact: true }).inputValue()).not.toBe('50')
await page.locator('.tag-style-picker input[value="compact"]').check()
await waitForSave(page, () => page.getByRole('button', { name: 'SAVE THREAD', exact: true }).click())
await expect(relationTag).toHaveClass(/\bcompact\b/)
@@ -144,7 +161,10 @@ test('a cloned Glass Harbor level can be solved without modifying its template',
await page.getByRole('button', { name: 'Close thread editor', exact: true }).click()
const solved = await (await request.get(`/api/levels/${playable.id}`)).json()
expect(solved.connections).toContainEqual(expect.objectContaining({ label: 'Proof Elias is the driver', tightness: 85, tagStyle: 'compact' }))
const eliasThread = solved.connections.find((connection: { label?: string }) => connection.label === 'Proof Elias is the driver')
expect(eliasThread).toEqual(expect.objectContaining({ tightness: 85, tagStyle: 'compact', tagPosition: expect.any(Number), tagOffset: expect.any(Number) }))
expect(eliasThread.tagPosition).not.toBe(50)
expect(Math.abs(eliasThread.tagOffset)).toBeLessThanOrEqual(19)
expect(solved.connections.filter((connection: { label?: string }) => connection.label === 'Proves Voss owns Warehouse 3')).toHaveLength(2)
const template = await (await request.get('/api/templates')).json() as { slug: string; currentVersion: number }[]