fad9760265
- 新增 Node 服务端(Hono + Hocuspocus + node:sqlite),数据保存在服务器 - 基于 Yjs 的多端实时同步,本机 IndexedDB 离线缓存,撤销只针对本机操作 - 空间(标签)有独立地址,增删改通过接口同步到所有设备 - 口令登录(签名 Cookie 会话、登录限流、同源校验) - 图片上传到服务器,过大照片上传前自动压缩 - 历史版本(自动/手动保存、一键恢复)、跨空间搜索、在线设备 - 随机油画加载动画(Painting Loaders,本地收录 p5 / p5.brush) - 选中样式与拖拽性能优化:拖动结束后才同步位置 - Docker / docker-compose 部署,README 与 CLAUDE.md 更新
192 lines
7.5 KiB
TypeScript
192 lines
7.5 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
||
|
||
import {
|
||
createNote,
|
||
createSpace,
|
||
expectNoteTitle,
|
||
login,
|
||
noteByTitle,
|
||
noteTitles,
|
||
openDevice,
|
||
} from './helpers'
|
||
|
||
test.describe('登录', () => {
|
||
test('未登录时跳转到登录页,口令错误会提示', async ({ page }) => {
|
||
await page.goto('/')
|
||
await expect(page).toHaveURL(/\/login/)
|
||
await page.getByLabel('登录口令').fill('wrong-password')
|
||
await page.getByLabel('登录口令').press('Enter')
|
||
await expect(page.getByRole('alert')).toHaveText('口令不正确')
|
||
})
|
||
|
||
test('登录后看到初始空间,退出后需要重新登录', async ({ page }) => {
|
||
await login(page)
|
||
await expectNoteTitle(page, '欢迎来到无界笺')
|
||
await page.getByRole('button', { name: '退出登录' }).click()
|
||
await expect(page).toHaveURL(/\/login/)
|
||
await page.goto('/')
|
||
await expect(page).toHaveURL(/\/login/)
|
||
})
|
||
})
|
||
|
||
test.describe('多端同步', () => {
|
||
test('一台设备的修改会实时出现在另一台设备上', async ({ page, browser }) => {
|
||
await login(page)
|
||
await createSpace(page, '同步测试')
|
||
const spaceUrl = page.url()
|
||
|
||
const other = await openDevice(browser, spaceUrl)
|
||
await expect(other.page.locator('.space-title')).toContainText('同步测试')
|
||
|
||
// 新建
|
||
await createNote(page, '来自设备 A')
|
||
await expectNoteTitle(other.page, '来自设备 A')
|
||
|
||
// 修改内容
|
||
const remoteCard = await noteByTitle(other.page, '来自设备 A')
|
||
await remoteCard.locator('.note-card__content').fill('设备 B 补充的一句话')
|
||
const localCard = await noteByTitle(page, '来自设备 A')
|
||
await expect(localCard.locator('.note-card__content')).toHaveValue('设备 B 补充的一句话')
|
||
|
||
// 换色
|
||
await localCard.locator('.color-picker button[data-color="mint"]').click()
|
||
await expect(remoteCard).toHaveAttribute('data-color', 'mint')
|
||
|
||
// 删除
|
||
await localCard.locator('.card-icon-button').click()
|
||
await page.getByRole('button', { name: '删除' }).click()
|
||
await expectNoteTitle(other.page, '来自设备 A', false)
|
||
|
||
await other.context.close()
|
||
})
|
||
|
||
test('新建的空间会出现在其他设备的列表里', async ({ page, browser }) => {
|
||
await login(page)
|
||
const other = await openDevice(browser)
|
||
await createSpace(page, '新的标签')
|
||
await expect(other.page.locator('.space-list')).toContainText('新的标签')
|
||
await expect(page.locator('.devices__button')).toHaveText(/2/)
|
||
await other.context.close()
|
||
})
|
||
|
||
test('刷新页面后内容仍在', async ({ page }) => {
|
||
await login(page)
|
||
await createSpace(page, '持久化')
|
||
await createNote(page, '刷新也不会丢')
|
||
await expect(page.getByText('已同步', { exact: true }).first()).toBeVisible()
|
||
await page.reload()
|
||
await expectNoteTitle(page, '刷新也不会丢')
|
||
})
|
||
|
||
test('撤销只影响本机的操作', async ({ page, browser }) => {
|
||
await login(page)
|
||
await createSpace(page, '撤销测试')
|
||
const other = await openDevice(browser, page.url())
|
||
|
||
await createNote(other.page, '别人的便笺')
|
||
await expectNoteTitle(page, '别人的便笺')
|
||
await createNote(page, '我的便笺')
|
||
await page.locator('.vue-flow__pane').click({ position: { x: 40, y: 300 } })
|
||
await page.keyboard.press('Control+Z')
|
||
await page.keyboard.press('Control+Z')
|
||
|
||
await expectNoteTitle(page, '我的便笺', false)
|
||
await expectNoteTitle(page, '别人的便笺')
|
||
await expectNoteTitle(other.page, '我的便笺', false)
|
||
await other.context.close()
|
||
})
|
||
})
|
||
|
||
test.describe('画布操作', () => {
|
||
test('粘贴文字会生成一张新便笺', async ({ page, context }) => {
|
||
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
|
||
await login(page)
|
||
await createSpace(page, '粘贴')
|
||
const before = await page.locator('.note-card').count()
|
||
|
||
await page.evaluate(() =>
|
||
navigator.clipboard.writeText('临时灵感\n把复制的文字直接变成一张新便笺。'),
|
||
)
|
||
await page.locator('.vue-flow__pane').click({ position: { x: 90, y: 300 } })
|
||
await page.keyboard.press('Control+V')
|
||
|
||
await expect(page.locator('.note-card')).toHaveCount(before + 1)
|
||
const card = await noteByTitle(page, '临时灵感')
|
||
await expect(card.locator('.note-card__content')).toHaveValue(
|
||
'临时灵感\n把复制的文字直接变成一张新便笺。',
|
||
)
|
||
})
|
||
|
||
test('上传图片后保存在服务器上', async ({ page }) => {
|
||
await login(page)
|
||
await createSpace(page, '图片')
|
||
// 1×1 PNG
|
||
const png = Buffer.from(
|
||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||
'base64',
|
||
)
|
||
await page.locator('input[type="file"]').setInputFiles({
|
||
name: '小图.png',
|
||
mimeType: 'image/png',
|
||
buffer: png,
|
||
})
|
||
const image = page.locator('.note-card__image-wrap img')
|
||
await expect(image).toHaveAttribute('src', /^\/api\/images\/[a-f0-9]{40}\.png$/)
|
||
const response = await page.request.get((await image.getAttribute('src'))!)
|
||
expect(response.status()).toBe(200)
|
||
expect(response.headers()['content-type']).toBe('image/png')
|
||
})
|
||
|
||
test('全局搜索可以跳到其他空间的卡片', async ({ page }) => {
|
||
await login(page)
|
||
await createSpace(page, '搜索来源')
|
||
await createNote(page, '藏在别处的萤火虫')
|
||
await expect(page.getByText('已同步', { exact: true }).first()).toBeVisible()
|
||
await createSpace(page, '搜索起点')
|
||
|
||
await page.getByLabel('搜索便笺').fill('萤火虫')
|
||
await page.getByRole('button', { name: /藏在别处的萤火虫/ }).click()
|
||
await expect(page.locator('.space-title')).toContainText('搜索来源')
|
||
await expect(page.locator('.note-card--selected .note-card__title')).toHaveValue(
|
||
'藏在别处的萤火虫',
|
||
)
|
||
})
|
||
})
|
||
|
||
test.describe('历史版本', () => {
|
||
test('可以恢复到之前保存的版本', async ({ page }) => {
|
||
await login(page)
|
||
await createSpace(page, '版本')
|
||
await createNote(page, '想留住的想法')
|
||
await expect(page.getByText('已同步', { exact: true }).first()).toBeVisible()
|
||
|
||
await page.getByRole('button', { name: '历史版本' }).click()
|
||
await page.getByRole('button', { name: '保存当前版本' }).click()
|
||
await expect(page.locator('.version-item', { hasText: '手动保存' })).toHaveCount(1)
|
||
await page.getByRole('button', { name: '完成' }).click()
|
||
|
||
const card = await noteByTitle(page, '想留住的想法')
|
||
await card.locator('.card-icon-button').click()
|
||
await page.getByRole('button', { name: '删除' }).click()
|
||
await expectNoteTitle(page, '想留住的想法', false)
|
||
await expect(page.getByText('已同步', { exact: true }).first()).toBeVisible()
|
||
|
||
await page.getByRole('button', { name: '历史版本' }).click()
|
||
const version = page.locator('.version-item', { hasText: '手动保存' })
|
||
await version.getByRole('button', { name: '恢复' }).click()
|
||
await version.getByRole('button', { name: '确认恢复' }).click()
|
||
await expectNoteTitle(page, '想留住的想法')
|
||
expect(await noteTitles(page)).toHaveLength(1)
|
||
})
|
||
})
|
||
|
||
test('加载动画会随机作画', async ({ browser }) => {
|
||
const context = await browser.newContext({ reducedMotion: 'no-preference' })
|
||
const page = await context.newPage()
|
||
await page.goto('/login')
|
||
const frame = page.frameLocator('.painting__tile iframe')
|
||
await expect(frame.locator('canvas').first()).toBeAttached({ timeout: 15_000 })
|
||
await expect(page.locator('.painting__caption')).toHaveText(/ · /)
|
||
await context.close()
|
||
})
|