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() })