feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
|
|
@ -0,0 +1,57 @@
|
|||
import { type Page, test } from '@playwright/test'
|
||||
|
||||
const SCREENSHOT_PATH = '/Users/luca/OpenClaw/ai-chat-final.jpg'
|
||||
|
||||
async function clickNoteListItem(page: Page): Promise<string> {
|
||||
return page.evaluate(() => {
|
||||
const items = document.querySelectorAll('[class*="cursor-pointer"]')
|
||||
const noteListItem = Array.from(items).find((el) => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
return rect.x > 249 && rect.x < 700 && rect.height > 40 && rect.width > 200
|
||||
})
|
||||
|
||||
if (!noteListItem) {
|
||||
return 'Nothing found'
|
||||
}
|
||||
|
||||
const noteListElement = noteListItem as HTMLElement
|
||||
noteListElement.click()
|
||||
const rect = noteListItem.getBoundingClientRect()
|
||||
return `Clicked: ${noteListItem.textContent?.trim().slice(0, 50)} at x=${rect.x}`
|
||||
})
|
||||
}
|
||||
|
||||
async function clickAiToolbarButton(page: Page): Promise<string> {
|
||||
return page.evaluate(() => {
|
||||
const aiButton = Array.from(document.querySelectorAll('button')).find((btn) =>
|
||||
btn.title?.includes('AI'),
|
||||
)
|
||||
|
||||
if (!aiButton) {
|
||||
return 'AI button not found'
|
||||
}
|
||||
|
||||
aiButton.click()
|
||||
return `Clicked: ${aiButton.title}`
|
||||
})
|
||||
}
|
||||
|
||||
test('screenshot AI chat panel', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 })
|
||||
await page.goto('/', { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(3000)
|
||||
|
||||
// Click a note item in the NoteList (x > 243, past the sidebar)
|
||||
// Note items have: cursor-pointer border-b border-[var(--border)]
|
||||
const clicked = await clickNoteListItem(page)
|
||||
console.log('Note click result:', clicked)
|
||||
await page.waitForTimeout(1200)
|
||||
|
||||
// Now find and click the AI button in the editor toolbar
|
||||
const aiBtn = await clickAiToolbarButton(page)
|
||||
console.log('AI btn result:', aiBtn)
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, type: 'jpeg', quality: 90 })
|
||||
console.log('Done')
|
||||
})
|
||||
69
product-source/hololake-platform/e2e/ai-chat.spec.ts
Normal file
69
product-source/hololake-platform/e2e/ai-chat.spec.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('AI chat panel: open, send message, close', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173')
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
// Click the first note in the list — note items are cursor-pointer divs inside .app__note-list
|
||||
const noteItem = page.locator('.app__note-list .cursor-pointer').first()
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
// Screenshot before opening AI chat
|
||||
await page.screenshot({ path: 'test-results/ai-chat-before.png', fullPage: true })
|
||||
|
||||
// Find the Sparkle button in the breadcrumb/info bar and click it
|
||||
const sparkleButton = page.locator('button[title="Open AI Chat"]')
|
||||
await expect(sparkleButton).toBeVisible({ timeout: 5000 })
|
||||
await sparkleButton.click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// AI Chat panel should be visible
|
||||
const aiChatHeader = page.locator('text=AI Chat')
|
||||
await expect(aiChatHeader).toBeVisible()
|
||||
|
||||
// Screenshot with AI chat panel open
|
||||
await page.screenshot({ path: 'test-results/ai-chat-open.png', fullPage: true })
|
||||
|
||||
// Context pills should be visible
|
||||
await expect(page.locator('text=Frontmatter')).toBeVisible()
|
||||
await expect(page.locator('text=Links')).toBeVisible()
|
||||
|
||||
// Type a message and send
|
||||
const textarea = page.locator('textarea[placeholder="Ask about this document..."]')
|
||||
await textarea.fill('Summarize this note')
|
||||
await page.locator('button[title="Send message"]').click()
|
||||
|
||||
// Should see user message
|
||||
await expect(page.locator('text=Summarize this note')).toBeVisible()
|
||||
|
||||
// Wait for typing indicator to appear
|
||||
await page.waitForTimeout(300)
|
||||
await page.screenshot({ path: 'test-results/ai-chat-typing.png', fullPage: true })
|
||||
|
||||
// Wait for mock response
|
||||
await page.waitForTimeout(1200)
|
||||
await expect(page.locator('text=words and links to')).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Screenshot with response
|
||||
await page.screenshot({ path: 'test-results/ai-chat-response.png', fullPage: true })
|
||||
|
||||
// Test quick action pill
|
||||
const expandButton = page.locator('button', { hasText: 'Expand' })
|
||||
await expandButton.click()
|
||||
await page.waitForTimeout(1800)
|
||||
await expect(page.locator('text=Add more detail to the introduction')).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Screenshot with quick action response
|
||||
await page.screenshot({ path: 'test-results/ai-chat-quick-action.png', fullPage: true })
|
||||
|
||||
// Close the panel using the X button in the AI Chat header
|
||||
await page.locator('button[title="Close AI Chat"]').last().click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Inspector should be back
|
||||
await expect(page.locator('text=Properties')).toBeVisible()
|
||||
|
||||
// Screenshot after closing
|
||||
await page.screenshot({ path: 'test-results/ai-chat-closed.png', fullPage: true })
|
||||
})
|
||||
27
product-source/hololake-platform/e2e/app.spec.ts
Normal file
27
product-source/hololake-platform/e2e/app.spec.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('app loads with four-panel layout', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
// Verify the four panels are present
|
||||
await expect(page.locator('.sidebar')).toBeVisible()
|
||||
await expect(page.locator('.note-list')).toBeVisible()
|
||||
await expect(page.locator('.editor')).toBeVisible()
|
||||
await expect(page.locator('.inspector')).toBeVisible()
|
||||
})
|
||||
|
||||
test('sidebar shows filters and section groups', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500) // Wait for mock data
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Laputa' })).toBeVisible()
|
||||
// Filters
|
||||
await expect(page.locator('.sidebar__filter-item').filter({ hasText: 'All Notes' })).toBeVisible()
|
||||
await expect(page.locator('.sidebar__filter-item').filter({ hasText: 'People' })).toBeVisible()
|
||||
await expect(page.locator('.sidebar__filter-item').filter({ hasText: 'Events' })).toBeVisible()
|
||||
// Section groups (use exact match to avoid collision with note list pills)
|
||||
await expect(page.locator('.sidebar__section-label', { hasText: 'PROJECTS' })).toBeVisible()
|
||||
await expect(page.locator('.sidebar__section-label', { hasText: 'EXPERIMENTS' })).toBeVisible()
|
||||
await expect(page.locator('.sidebar__section-label', { hasText: 'RESPONSIBILITIES' })).toBeVisible()
|
||||
await expect(page.locator('.sidebar__section-label', { hasText: 'PROCEDURES' })).toBeVisible()
|
||||
})
|
||||
63
product-source/hololake-platform/e2e/auto-save.spec.ts
Normal file
63
product-source/hololake-platform/e2e/auto-save.spec.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.use({ baseURL: 'http://localhost:5239' })
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(2000) // Wait for vault data to load
|
||||
})
|
||||
|
||||
test('editor loads and renders note content for editing', async ({ page }) => {
|
||||
await page.screenshot({ path: 'test-results/save-01-initial.png', fullPage: true })
|
||||
|
||||
// 1. Click a note in the note list panel
|
||||
const noteList = page.locator('.app__note-list')
|
||||
await expect(noteList).toBeVisible({ timeout: 5000 })
|
||||
const firstNote = noteList.locator('div.cursor-pointer').first()
|
||||
await expect(firstNote).toBeVisible({ timeout: 5000 })
|
||||
await firstNote.click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// 2. Verify the BlockNote editor is visible with content
|
||||
const editor = page.locator('.bn-editor')
|
||||
await expect(editor).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Verify the editor is contenteditable (ready for editing)
|
||||
const isEditable = await editor.getAttribute('contenteditable')
|
||||
expect(isEditable).toBe('true')
|
||||
|
||||
await page.screenshot({ path: 'test-results/save-02-note-open.png', fullPage: true })
|
||||
|
||||
// 3. Verify the editor has content (not empty)
|
||||
const editorText = await page.evaluate(() => {
|
||||
const el = document.querySelector('.bn-editor')
|
||||
return el?.textContent ?? ''
|
||||
})
|
||||
expect(editorText.length).toBeGreaterThan(10)
|
||||
|
||||
// 4. Verify tab bar shows the active note
|
||||
const tabBar = page.locator('.editor')
|
||||
await expect(tabBar).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/save-03-editor-ready.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('Cmd+S keyboard shortcut triggers save toast', async ({ page }) => {
|
||||
// Open a note
|
||||
const noteList = page.locator('.app__note-list')
|
||||
await expect(noteList).toBeVisible({ timeout: 5000 })
|
||||
const firstNote = noteList.locator('div.cursor-pointer').first()
|
||||
await firstNote.click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Press Cmd+S — shows either "Saved" or "Nothing to save" depending on
|
||||
// whether BlockNote's onChange fired from prior interactions
|
||||
await page.keyboard.press('Meta+s')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Verify a save-related toast appears (the shortcut was handled)
|
||||
const toast = page.locator('text=/Saved|Nothing to save/')
|
||||
await expect(toast).toBeVisible({ timeout: 3000 })
|
||||
|
||||
await page.screenshot({ path: 'test-results/save-04-cmd-s.png', fullPage: true })
|
||||
})
|
||||
260
product-source/hololake-platform/e2e/core-flows.spec.ts
Normal file
260
product-source/hololake-platform/e2e/core-flows.spec.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500) // Wait for mock data
|
||||
})
|
||||
|
||||
// --- Flow 1: Open app, click note, verify editor shows content ---
|
||||
|
||||
test('clicking a note opens it in the editor with content', async ({ page }) => {
|
||||
// Click "Build Laputa App" in the note list
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Tab should appear and be active
|
||||
await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/)
|
||||
|
||||
// Editor should show note content (heading in live preview)
|
||||
await expect(page.locator('.cm-editor')).toBeVisible()
|
||||
|
||||
// Inspector should show properties
|
||||
await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/core-open-note.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('editor shows markdown content with live preview', async ({ page }) => {
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// CodeMirror should render the content
|
||||
const editor = page.locator('.cm-editor')
|
||||
await expect(editor).toBeVisible()
|
||||
|
||||
// Should contain the heading text (rendered by live preview)
|
||||
await expect(page.locator('.cm-content')).toContainText('Build Laputa App')
|
||||
})
|
||||
|
||||
// --- Flow 2: Sidebar filter changes note list ---
|
||||
|
||||
test('sidebar filter: People shows only Person entries', async ({ page }) => {
|
||||
await page.locator('.sidebar__filter-item', { hasText: 'People' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('.note-list__count')).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).toBeVisible()
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('sidebar filter: Events shows only Event entries', async ({ page }) => {
|
||||
await page.locator('.sidebar__filter-item', { hasText: 'Events' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('.note-list__count')).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Laputa App Design Session' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('sidebar filter: clicking back to All Notes restores full list', async ({ page }) => {
|
||||
await page.locator('.sidebar__filter-item', { hasText: 'People' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.note-list__count')).toHaveText('1')
|
||||
|
||||
await page.locator('.sidebar__filter-item', { hasText: 'All Notes' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.note-list__count')).toHaveText('12')
|
||||
})
|
||||
|
||||
// --- Flow 3: Search for a note ---
|
||||
|
||||
test('search filters notes by title', async ({ page }) => {
|
||||
await page.fill('.note-list__search-input', 'stock')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('.note-list__count')).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Stock Screener' })).toBeVisible()
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('clearing search restores all results', async ({ page }) => {
|
||||
await page.fill('.note-list__search-input', 'stock')
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.note-list__count')).toHaveText('1')
|
||||
|
||||
await page.fill('.note-list__search-input', '')
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.note-list__count')).toHaveText('12')
|
||||
})
|
||||
|
||||
// --- Flow 4: Open multiple tabs and switch between them ---
|
||||
|
||||
test('opening multiple notes creates multiple tabs', async ({ page }) => {
|
||||
// Open first note
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Open second note
|
||||
await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Both tabs should exist
|
||||
const tabs = page.locator('.editor__tab')
|
||||
await expect(tabs).toHaveCount(2)
|
||||
|
||||
// Second tab should be active
|
||||
await expect(page.locator('.editor__tab--active')).toHaveText(/Grow Newsletter/)
|
||||
|
||||
await page.screenshot({ path: 'test-results/core-multi-tabs.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('clicking a tab switches to it', async ({ page }) => {
|
||||
// Open two notes
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Switch back to first tab
|
||||
await page.locator('.editor__tab', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// First tab should now be active
|
||||
await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/)
|
||||
|
||||
// Editor should show first note's content
|
||||
await expect(page.locator('.cm-content')).toContainText('Build Laputa App')
|
||||
})
|
||||
|
||||
test('closing a tab removes it and switches to adjacent', async ({ page }) => {
|
||||
// Open two notes
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
await page.locator('.note-list__item', { hasText: 'Grow Newsletter' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Close active tab (Grow Newsletter)
|
||||
await page.locator('.editor__tab--active .editor__tab-close').click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Should have 1 tab left
|
||||
await expect(page.locator('.editor__tab')).toHaveCount(1)
|
||||
await expect(page.locator('.editor__tab--active')).toHaveText(/Build Laputa App/)
|
||||
})
|
||||
|
||||
// --- Flow 5: Inspector shows correct properties ---
|
||||
|
||||
test('inspector shows properties for selected note', async ({ page }) => {
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Type
|
||||
await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible()
|
||||
// Status
|
||||
await expect(page.locator('.inspector__status-pill', { hasText: 'Active' })).toBeVisible()
|
||||
// Owner
|
||||
await expect(page.locator('.inspector__prop-value', { hasText: 'Luca Rossi' })).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/core-inspector.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('inspector shows relationships', async ({ page }) => {
|
||||
// Open a note with relationships
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Should show "Related to" relationships
|
||||
await expect(page.locator('.inspector__section', { hasText: 'Relationships' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('inspector shows backlinks', async ({ page }) => {
|
||||
// Open a note that has backlinks (Build Laputa App is referenced by Facebook Ads and Budget Allocation)
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Backlinks section should show
|
||||
const backlinksSection = page.locator('.inspector__section', { hasText: 'Backlinks' })
|
||||
await expect(backlinksSection).toBeVisible()
|
||||
})
|
||||
|
||||
test('inspector shows git history', async ({ page }) => {
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Should show commits
|
||||
await expect(page.locator('.inspector__commit-hash').first()).toBeVisible()
|
||||
await expect(page.locator('.inspector__commit-msg').first()).toContainText('26q1-laputa-app')
|
||||
})
|
||||
|
||||
test('inspector updates when switching tabs', async ({ page }) => {
|
||||
// Open first note
|
||||
await page.locator('.note-list__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.locator('.inspector__prop-value', { hasText: 'Project' })).toBeVisible()
|
||||
|
||||
// Open second note with different type — use title locator for precision
|
||||
await page.locator('.note-list__item').filter({ has: page.locator('.note-list__title', { hasText: 'Matteo Cellini' }) }).click()
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.locator('.inspector__prop-value', { hasText: 'Person' })).toBeVisible()
|
||||
})
|
||||
|
||||
// --- Flow 6: Note list preview snippets ---
|
||||
|
||||
test('note list items show preview snippets', async ({ page }) => {
|
||||
// Check that snippets are visible
|
||||
const snippet = page.locator('.note-list__snippet').first()
|
||||
await expect(snippet).toBeVisible()
|
||||
// Snippet should have some text content
|
||||
const text = await snippet.textContent()
|
||||
expect(text!.length).toBeGreaterThan(10)
|
||||
})
|
||||
|
||||
// --- Flow 7: Create note and verify it appears ---
|
||||
|
||||
test('full create note flow', async ({ page }) => {
|
||||
// Count before
|
||||
const countBefore = await page.locator('.note-list__count').textContent()
|
||||
|
||||
// Create new note
|
||||
await page.click('.note-list__add-btn')
|
||||
await page.waitForTimeout(200)
|
||||
await page.fill('.create-dialog__input', 'E2E Test Note')
|
||||
await page.click('.create-dialog__type-btn:text("Experiment")')
|
||||
await page.click('.create-dialog__btn--create')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Count should increase
|
||||
const countAfter = await page.locator('.note-list__count').textContent()
|
||||
expect(parseInt(countAfter!, 10)).toBe(parseInt(countBefore!, 10) + 1)
|
||||
|
||||
// Note should be opened in editor
|
||||
await expect(page.locator('.editor__tab--active')).toHaveText(/E2E Test Note/)
|
||||
|
||||
await page.screenshot({ path: 'test-results/core-create-note.png', fullPage: true })
|
||||
})
|
||||
|
||||
// --- Flow 8: Wiki-link navigation ---
|
||||
|
||||
test('clicking a wikilink opens the target note in a new tab', async ({ page }) => {
|
||||
// Open "Manage Sponsorships" which contains [[Matteo Cellini]] wikilink
|
||||
await page.locator('.note-list__item', { hasText: 'Manage Sponsorships' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Verify we opened the right note
|
||||
await expect(page.locator('.editor__tab--active')).toHaveText(/Manage Sponsorships/)
|
||||
|
||||
// Click the wikilink — use mouse.click to fire real mousedown
|
||||
const wikilink = page.locator('.cm-wikilink', { hasText: 'Matteo Cellini' })
|
||||
await expect(wikilink).toBeVisible()
|
||||
const box = await wikilink.boundingBox()
|
||||
expect(box).not.toBeNull()
|
||||
await page.mouse.click(box!.x + box!.width / 2, box!.y + box!.height / 2)
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// New tab should open with the target note active
|
||||
await expect(page.locator('.editor__tab--active')).toHaveText(/Matteo Cellini/)
|
||||
|
||||
// Editor should show the target note's content
|
||||
await expect(page.locator('.cm-content')).toContainText('Matteo Cellini')
|
||||
|
||||
await page.screenshot({ path: 'test-results/core-wikilink-nav.png', fullPage: true })
|
||||
})
|
||||
57
product-source/hololake-platform/e2e/create-note.spec.ts
Normal file
57
product-source/hololake-platform/e2e/create-note.spec.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('clicking + button opens create note dialog', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click the + button
|
||||
await page.click('.note-list__add-btn')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Dialog should be visible
|
||||
await expect(page.locator('.create-dialog')).toBeVisible()
|
||||
await expect(page.locator('.create-dialog__title')).toHaveText('Create New Note')
|
||||
|
||||
await page.screenshot({ path: 'test-results/create-dialog.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('create a new note via dialog', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open dialog
|
||||
await page.click('.note-list__add-btn')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Type a title
|
||||
await page.fill('.create-dialog__input', 'My Test Note')
|
||||
|
||||
// Select "Project" type
|
||||
await page.click('.create-dialog__type-btn:text("Project")')
|
||||
|
||||
// Click Create
|
||||
await page.click('.create-dialog__btn--create')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.locator('.create-dialog')).not.toBeVisible()
|
||||
|
||||
// New note should appear in the list and be opened in editor
|
||||
await expect(page.locator('.note-list__item:has-text("My Test Note")').first()).toBeVisible()
|
||||
await expect(page.locator('.editor__tab--active:has-text("My Test Note")')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/create-note-result.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('escape closes create dialog', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.click('.note-list__add-btn')
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.create-dialog')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.create-dialog')).not.toBeVisible()
|
||||
})
|
||||
120
product-source/hololake-platform/e2e/create-type.spec.ts
Normal file
120
product-source/hololake-platform/e2e/create-type.spec.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Create New Type Feature', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('http://localhost:5203')
|
||||
await page.waitForSelector('text=All Notes')
|
||||
})
|
||||
|
||||
test('clicking + on Types section opens Create Type dialog', async ({ page }) => {
|
||||
// Hover over the Types section to reveal the + button
|
||||
const typesSection = page.locator('text=Types').first()
|
||||
await typesSection.hover()
|
||||
|
||||
// Click the + button next to Types
|
||||
const createTypeBtn = page.locator('[title="New Type"]')
|
||||
await createTypeBtn.click()
|
||||
|
||||
// Dialog should open with correct title and elements
|
||||
await expect(page.locator('text=Create New Type')).toBeVisible()
|
||||
await expect(page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]')).toBeVisible()
|
||||
await expect(page.locator('text=Creates a type document')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/create-type-dialog.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('Create button is disabled when name is empty', async ({ page }) => {
|
||||
const typesSection = page.locator('text=Types').first()
|
||||
await typesSection.hover()
|
||||
await page.locator('[title="New Type"]').click()
|
||||
|
||||
const createBtn = page.locator('button:has-text("Create")')
|
||||
await expect(createBtn).toBeDisabled()
|
||||
})
|
||||
|
||||
test('can create a new type and it appears in sidebar', async ({ page }) => {
|
||||
// Open Create Type dialog
|
||||
const typesSection = page.locator('text=Types').first()
|
||||
await typesSection.hover()
|
||||
await page.locator('[title="New Type"]').click()
|
||||
|
||||
// Type a name and submit
|
||||
await page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]').fill('Workout')
|
||||
await page.locator('button:has-text("Create")').click()
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.locator('text=Create New Type')).not.toBeVisible()
|
||||
|
||||
// New type should appear as a sidebar section (pluralized)
|
||||
await expect(page.locator('text=Workouts')).toBeVisible({ timeout: 3000 })
|
||||
|
||||
// The type document should open in the editor
|
||||
await expect(page.locator('text=Workout').first()).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/after-create-type.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('newly created type appears in Create Note dialog type selector', async ({ page }) => {
|
||||
// First, create a custom type
|
||||
const typesSection = page.locator('text=Types').first()
|
||||
await typesSection.hover()
|
||||
await page.locator('[title="New Type"]').click()
|
||||
await page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]').fill('Workout')
|
||||
await page.locator('button:has-text("Create")').click()
|
||||
|
||||
// Now open Create Note dialog
|
||||
await page.keyboard.press('Meta+n')
|
||||
await page.waitForSelector('text=Create New Note')
|
||||
|
||||
// Built-in types should be visible
|
||||
await expect(page.locator('button:has-text("Note")')).toBeVisible()
|
||||
await expect(page.locator('button:has-text("Project")')).toBeVisible()
|
||||
|
||||
// Our custom type should also be visible
|
||||
await expect(page.locator('button:has-text("Workout")')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/create-note-with-custom-type.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('can create an instance of a custom type', async ({ page }) => {
|
||||
// First, create a custom type
|
||||
const typesSection = page.locator('text=Types').first()
|
||||
await typesSection.hover()
|
||||
await page.locator('[title="New Type"]').click()
|
||||
await page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]').fill('Workout')
|
||||
await page.locator('button:has-text("Create")').click()
|
||||
await expect(page.locator('text=Workouts')).toBeVisible({ timeout: 3000 })
|
||||
|
||||
// Hover over the new Workouts section and click +
|
||||
const workoutsSection = page.locator('text=Workouts').first()
|
||||
await workoutsSection.hover()
|
||||
await page.locator('[title="New Workout"]').click()
|
||||
|
||||
// Create Note dialog should open
|
||||
await expect(page.locator('text=Create New Note')).toBeVisible()
|
||||
|
||||
// Type a title and create
|
||||
await page.locator('input[placeholder="Enter note title..."]').fill('Morning Run')
|
||||
await page.locator('button:has-text("Create")').last().click()
|
||||
|
||||
// The note should open in editor
|
||||
await expect(page.locator('text=Morning Run').first()).toBeVisible({ timeout: 3000 })
|
||||
|
||||
await page.screenshot({ path: 'test-results/custom-type-instance.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('Cancel closes the dialog without creating', async ({ page }) => {
|
||||
const typesSection = page.locator('text=Types').first()
|
||||
await typesSection.hover()
|
||||
await page.locator('[title="New Type"]').click()
|
||||
|
||||
await page.locator('input[placeholder="e.g. Recipe, Book, Habit..."]').fill('ShouldNotExist')
|
||||
await page.locator('button:has-text("Cancel")').click()
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.locator('text=Create New Type')).not.toBeVisible()
|
||||
|
||||
// Type should NOT appear in sidebar
|
||||
await expect(page.locator('text=ShouldNotExists')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.use({ baseURL: 'http://localhost:5201' })
|
||||
|
||||
/**
|
||||
* Regression test: editor content must appear after clicking a note.
|
||||
*
|
||||
* Root cause: BlockNote's replaceBlocks/insertBlocks internally calls flushSync,
|
||||
* which fails silently when invoked from inside React's useEffect lifecycle.
|
||||
* Fix: defer the content swap via queueMicrotask.
|
||||
*/
|
||||
test('editor content appears on first note click', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text())
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
// Wait for note list to load
|
||||
await page.waitForSelector('.app__note-list .cursor-pointer', { timeout: 15000 })
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Click the first note in the note list
|
||||
const noteList = page.locator('.app__note-list')
|
||||
const firstNote = noteList.locator('.cursor-pointer').first()
|
||||
await firstNote.click()
|
||||
|
||||
// Wait for ProseMirror editor to have content
|
||||
const pm = page.locator('.ProseMirror')
|
||||
await expect(async () => {
|
||||
const text = await pm.textContent()
|
||||
expect(text?.trim().length, 'Editor content should not be empty').toBeGreaterThan(5)
|
||||
}).toPass({ timeout: 5000 })
|
||||
|
||||
// Verify no flushSync errors appeared
|
||||
const flushSyncErrors = errors.filter(e => e.includes('flushSync'))
|
||||
expect(flushSyncErrors, 'No flushSync-inside-lifecycle errors should occur').toHaveLength(0)
|
||||
})
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('editor panel has proper width regardless of title length', async ({ page }) => {
|
||||
await page.goto('http://localhost:5204')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Click on a note with a short title from the note list
|
||||
const noteItems = page.locator('.note-list__item')
|
||||
const count = await noteItems.count()
|
||||
|
||||
if (count > 0) {
|
||||
// Click the first note
|
||||
await noteItems.first().click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Measure editor container width
|
||||
const editorContainer = page.locator('.editor__blocknote-container')
|
||||
const box = await editorContainer.boundingBox()
|
||||
|
||||
// The editor should have a reasonable width (at least 300px)
|
||||
expect(box).toBeTruthy()
|
||||
expect(box!.width).toBeGreaterThan(300)
|
||||
|
||||
// Screenshot for visual verification
|
||||
await page.screenshot({ path: 'test-results/editor-min-width.png', fullPage: true })
|
||||
|
||||
// Also check that .bn-container fills the editor width
|
||||
const bnContainer = page.locator('.editor__blocknote-container .bn-container')
|
||||
const bnBox = await bnContainer.boundingBox()
|
||||
expect(bnBox).toBeTruthy()
|
||||
expect(bnBox!.width).toBeGreaterThan(300)
|
||||
}
|
||||
})
|
||||
82
product-source/hololake-platform/e2e/filtering.spec.ts
Normal file
82
product-source/hololake-platform/e2e/filtering.spec.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500) // Wait for mock data
|
||||
})
|
||||
|
||||
test('All Notes shows all entries', async ({ page }) => {
|
||||
const count = page.locator('.note-list__count')
|
||||
await expect(count).toHaveText('12')
|
||||
})
|
||||
|
||||
test('clicking People filter shows only people', async ({ page }) => {
|
||||
await page.click('text=People')
|
||||
await page.waitForTimeout(100)
|
||||
const count = page.locator('.note-list__count')
|
||||
await expect(count).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/filter-people.png' })
|
||||
})
|
||||
|
||||
test('clicking Events filter shows only events', async ({ page }) => {
|
||||
await page.click('text=Events')
|
||||
await page.waitForTimeout(100)
|
||||
const count = page.locator('.note-list__count')
|
||||
await expect(count).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Laputa App Design Session' })).toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/filter-events.png' })
|
||||
})
|
||||
|
||||
test('clicking PROJECTS header shows all projects', async ({ page }) => {
|
||||
await page.click('text=PROJECTS')
|
||||
await page.waitForTimeout(100)
|
||||
const count = page.locator('.note-list__count')
|
||||
await expect(count).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/filter-projects.png' })
|
||||
})
|
||||
|
||||
test('clicking specific entity shows it pinned with children', async ({ page }) => {
|
||||
await page.locator('.sidebar__item', { hasText: 'Build Laputa App' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
// Pinned entity + children that belongTo this project
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible()
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Facebook Ads Strategy' })).toBeVisible()
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Budget Allocation' })).toBeVisible()
|
||||
await expect(page.locator('.note-list__item--pinned')).toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/filter-entity.png' })
|
||||
})
|
||||
|
||||
test('clicking topic shows entries related to that topic', async ({ page }) => {
|
||||
await page.locator('.sidebar__topic-item', { hasText: 'Software Development' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/filter-topic.png' })
|
||||
})
|
||||
|
||||
test('search bar filters by title substring', async ({ page }) => {
|
||||
await page.fill('.note-list__search-input', 'budget')
|
||||
await page.waitForTimeout(100)
|
||||
const count = page.locator('.note-list__count')
|
||||
await expect(count).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Budget Allocation' })).toBeVisible()
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/search-budget.png' })
|
||||
})
|
||||
|
||||
test('type filter pills narrow results', async ({ page }) => {
|
||||
// Click "Projects" pill
|
||||
await page.locator('.note-list__pill', { hasText: 'Projects' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
const count = page.locator('.note-list__count')
|
||||
await expect(count).toHaveText('1')
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible()
|
||||
await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).not.toBeVisible()
|
||||
await page.screenshot({ path: 'test-results/pill-projects.png' })
|
||||
|
||||
// Click "All" to reset
|
||||
await page.locator('.note-list__pill', { hasText: 'All' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await expect(count).toHaveText('12')
|
||||
})
|
||||
19
product-source/hololake-platform/e2e/find-selectors.spec.ts
Normal file
19
product-source/hololake-platform/e2e/find-selectors.spec.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { test } from '@playwright/test'
|
||||
|
||||
test('find note selectors', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 })
|
||||
await page.goto('/', { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(3000)
|
||||
|
||||
// Get all clickable elements with their text
|
||||
const info = await page.evaluate(() => {
|
||||
const items = document.querySelectorAll('[class*="cursor-pointer"]')
|
||||
return Array.from(items).slice(0, 20).map(el => ({
|
||||
tag: el.tagName,
|
||||
text: el.textContent?.trim().slice(0, 50),
|
||||
cls: el.className.toString().slice(0, 80),
|
||||
rect: el.getBoundingClientRect().toJSON()
|
||||
}))
|
||||
})
|
||||
console.log(JSON.stringify(info, null, 2))
|
||||
})
|
||||
182
product-source/hololake-platform/e2e/image-drag-drop.spec.ts
Normal file
182
product-source/hololake-platform/e2e/image-drag-drop.spec.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
// Minimal valid PNG: 1x1 red pixel
|
||||
const TEST_PNG_BASE64 =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
|
||||
|
||||
function createTestImage(filepath: string) {
|
||||
fs.mkdirSync(path.dirname(filepath), { recursive: true })
|
||||
fs.writeFileSync(filepath, Buffer.from(TEST_PNG_BASE64, 'base64'))
|
||||
}
|
||||
|
||||
test('drag & drop image into editor inserts image block', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Open a note
|
||||
await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 })
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const editor = page.locator('.bn-editor')
|
||||
await expect(editor).toBeVisible({ timeout: 10000 })
|
||||
await editor.click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Create a test image file
|
||||
const testImagePath = path.join(process.cwd(), 'test-results', 'drag-test-image.png')
|
||||
createTestImage(testImagePath)
|
||||
|
||||
// Screenshot before
|
||||
await page.screenshot({ path: 'test-results/drag-drop-before.png', fullPage: true })
|
||||
|
||||
// Simulate drag-and-drop of a file into the editor
|
||||
// Playwright supports dispatching drag events with DataTransfer
|
||||
const editorContainer = page.locator('.editor__blocknote-container')
|
||||
const box = await editorContainer.boundingBox()
|
||||
expect(box).toBeTruthy()
|
||||
|
||||
// Use Playwright's page.dispatchEvent with a custom script to simulate file drop
|
||||
await page.evaluate(async ({ base64, x, y }) => {
|
||||
const container = document.querySelector('.editor__blocknote-container')
|
||||
if (!container) throw new Error('Editor container not found')
|
||||
|
||||
// Convert base64 to Uint8Array
|
||||
const binary = atob(base64)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
||||
|
||||
// Create a File object
|
||||
const file = new File([bytes], 'drag-test-image.png', { type: 'image/png' })
|
||||
|
||||
// Create DataTransfer with the file
|
||||
const dt = new DataTransfer()
|
||||
dt.items.add(file)
|
||||
|
||||
// Dispatch dragover first (to set the drop effect)
|
||||
const dragOverEvent = new DragEvent('dragover', {
|
||||
dataTransfer: dt,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
})
|
||||
container.dispatchEvent(dragOverEvent)
|
||||
|
||||
// Then dispatch drop
|
||||
const dropEvent = new DragEvent('drop', {
|
||||
dataTransfer: dt,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
})
|
||||
container.dispatchEvent(dropEvent)
|
||||
}, {
|
||||
base64: TEST_PNG_BASE64,
|
||||
x: box!.x + box!.width / 2,
|
||||
y: box!.y + box!.height / 2,
|
||||
})
|
||||
|
||||
// Wait for the image to be uploaded and inserted
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
// Verify: image element exists in the editor
|
||||
const images = page.locator('.bn-editor img')
|
||||
await expect(images.first()).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Verify: image uses data URL (browser mode)
|
||||
const src = await images.first().getAttribute('src')
|
||||
expect(src).toMatch(/^data:/)
|
||||
|
||||
await page.screenshot({ path: 'test-results/drag-drop-after.png', fullPage: true })
|
||||
|
||||
// Clean up
|
||||
if (fs.existsSync(testImagePath)) fs.unlinkSync(testImagePath)
|
||||
})
|
||||
|
||||
test('drop zone overlay appears during image drag', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Open a note
|
||||
await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 })
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const editor = page.locator('.bn-editor')
|
||||
await expect(editor).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const editorContainer = page.locator('.editor__blocknote-container')
|
||||
const box = await editorContainer.boundingBox()
|
||||
expect(box).toBeTruthy()
|
||||
|
||||
// Simulate dragover with an image file to trigger overlay
|
||||
await page.evaluate(({ x, y }) => {
|
||||
const container = document.querySelector('.editor__blocknote-container')
|
||||
if (!container) throw new Error('Editor container not found')
|
||||
|
||||
const file = new File([new Uint8Array(1)], 'test.png', { type: 'image/png' })
|
||||
const dt = new DataTransfer()
|
||||
dt.items.add(file)
|
||||
|
||||
const event = new DragEvent('dragover', {
|
||||
dataTransfer: dt,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
})
|
||||
container.dispatchEvent(event)
|
||||
}, { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 })
|
||||
|
||||
// The drop overlay should be visible
|
||||
const overlay = page.locator('.editor__drop-overlay')
|
||||
await expect(overlay).toBeVisible({ timeout: 2000 })
|
||||
await expect(overlay).toContainText('Drop image here')
|
||||
|
||||
await page.screenshot({ path: 'test-results/drag-drop-overlay.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('non-image file drop is ignored', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Open a note
|
||||
await page.locator('[data-testid="type-icon"]').first().click({ timeout: 10000 })
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const editor = page.locator('.bn-editor')
|
||||
await expect(editor).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const editorContainer = page.locator('.editor__blocknote-container')
|
||||
const box = await editorContainer.boundingBox()
|
||||
expect(box).toBeTruthy()
|
||||
|
||||
// Count images before
|
||||
const imagesBefore = await page.locator('.bn-editor img').count()
|
||||
|
||||
// Simulate dropping a text file
|
||||
await page.evaluate(({ x, y }) => {
|
||||
const container = document.querySelector('.editor__blocknote-container')
|
||||
if (!container) throw new Error('Editor container not found')
|
||||
|
||||
const file = new File(['not an image'], 'readme.txt', { type: 'text/plain' })
|
||||
const dt = new DataTransfer()
|
||||
dt.items.add(file)
|
||||
|
||||
container.dispatchEvent(new DragEvent('drop', {
|
||||
dataTransfer: dt,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
}))
|
||||
}, { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 })
|
||||
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// No new images should have been added
|
||||
const imagesAfter = await page.locator('.bn-editor img').count()
|
||||
expect(imagesAfter).toBe(imagesBefore)
|
||||
})
|
||||
134
product-source/hololake-platform/e2e/image-upload.spec.ts
Normal file
134
product-source/hololake-platform/e2e/image-upload.spec.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { test, expect, type Page } from '@playwright/test'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import {
|
||||
createFixtureVaultCopy,
|
||||
openFixtureVault,
|
||||
removeFixtureVaultCopy,
|
||||
} from '../tests/helpers/fixtureVault'
|
||||
|
||||
// Minimal valid PNG: 1x1 red pixel
|
||||
const TEST_PNG_BASE64 =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
|
||||
|
||||
function createTestPng(filepath: string) {
|
||||
fs.mkdirSync(path.dirname(filepath), { recursive: true })
|
||||
fs.writeFileSync(filepath, Buffer.from(TEST_PNG_BASE64, 'base64'))
|
||||
}
|
||||
|
||||
let tempVaultDir: string
|
||||
|
||||
async function openImageTestNote(page: Page) {
|
||||
await page.locator('[data-testid="note-list-container"]').getByText('Alpha Project', { exact: true }).click()
|
||||
|
||||
const editor = page.locator('.bn-editor')
|
||||
await expect(editor).toBeVisible({ timeout: 10000 })
|
||||
return editor
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testInfo.setTimeout(60_000)
|
||||
tempVaultDir = createFixtureVaultCopy()
|
||||
await openFixtureVault(page, tempVaultDir)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
removeFixtureVaultCopy(tempVaultDir)
|
||||
})
|
||||
|
||||
test('image upload via file picker displays image with data URL', async ({ page }) => {
|
||||
const editor = await openImageTestNote(page)
|
||||
await editor.click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Insert image block via slash command
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.type('/image', { delay: 80 })
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Select Image from slash menu (press Enter to pick first match)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Verify Upload tab is available (uploadFile is configured)
|
||||
const fileInput = page.locator('input[type="file"]')
|
||||
expect(await fileInput.count()).toBeGreaterThan(0)
|
||||
|
||||
// Upload a test image
|
||||
const testImagePath = path.join(process.cwd(), 'test-results', 'test-image.png')
|
||||
createTestPng(testImagePath)
|
||||
|
||||
await fileInput.first().setInputFiles(testImagePath)
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
// Verify: image element exists in the editor
|
||||
const images = page.locator('.bn-editor img')
|
||||
const imageCount = await images.count()
|
||||
expect(imageCount).toBeGreaterThan(0)
|
||||
|
||||
// Verify: image uses data URL (stable, survives reload in dev mode)
|
||||
const src = await images.first().getAttribute('src')
|
||||
expect(src).toMatch(/^data:/)
|
||||
|
||||
// Verify: no "Loading..." elements remain
|
||||
const loadingEls = page.locator('.bn-file-loading-preview')
|
||||
expect(await loadingEls.count()).toBe(0)
|
||||
|
||||
await page.screenshot({ path: 'test-results/image-upload-after.png', fullPage: true })
|
||||
|
||||
if (fs.existsSync(testImagePath)) fs.unlinkSync(testImagePath)
|
||||
})
|
||||
|
||||
test('image paste into editor inserts image block', async ({ page }) => {
|
||||
const editor = await openImageTestNote(page)
|
||||
await editor.click()
|
||||
|
||||
await page.evaluate((base64) => {
|
||||
const editorElement = document.querySelector('.bn-editor')
|
||||
if (!editorElement) throw new Error('Editor not found')
|
||||
|
||||
const binary = atob(base64)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
||||
|
||||
const file = new File([bytes], 'pasted-image.png', { type: 'image/png' })
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.items.add(file)
|
||||
editorElement.dispatchEvent(new ClipboardEvent('paste', {
|
||||
clipboardData,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}))
|
||||
}, TEST_PNG_BASE64)
|
||||
|
||||
const images = page.locator('.bn-editor img')
|
||||
await expect(images.first()).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const src = await images.first().getAttribute('src')
|
||||
expect(src).toMatch(/^data:/)
|
||||
})
|
||||
|
||||
test('editor has uploadFile configured (no error on image block insert)', async ({ page }) => {
|
||||
const editor = await openImageTestNote(page)
|
||||
|
||||
// Capture console errors
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (err) => errors.push(err.message))
|
||||
|
||||
// Insert an image block via slash command
|
||||
await editor.click()
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('/image', { delay: 30 })
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Press Enter to select Image
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.screenshot({ path: 'test-results/image-block-inserted.png', fullPage: true })
|
||||
|
||||
// No errors related to upload should have occurred
|
||||
const uploadErrors = errors.filter(e => e.includes('upload'))
|
||||
expect(uploadErrors).toHaveLength(0)
|
||||
})
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Cmd+N opens create note dialog', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Meta+n')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('.create-dialog')).toBeVisible()
|
||||
await expect(page.locator('.create-dialog__title')).toHaveText('Create New Note')
|
||||
})
|
||||
|
||||
test('Cmd+S shows save toast', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Meta+s')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('.toast')).toBeVisible()
|
||||
await expect(page.locator('.toast')).toHaveText('Saved')
|
||||
|
||||
await page.screenshot({ path: 'test-results/save-toast.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('Cmd+W closes the active tab', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open a note
|
||||
await page.click('.note-list__item')
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.locator('.editor__tab--active')).toBeVisible()
|
||||
|
||||
// Close it with Cmd+W
|
||||
await page.keyboard.press('Meta+w')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Tab should be gone, placeholder should show
|
||||
await expect(page.locator('.editor__tab')).not.toBeVisible()
|
||||
await expect(page.locator('.editor__placeholder')).toBeVisible()
|
||||
})
|
||||
88
product-source/hololake-platform/e2e/quick-open.spec.ts
Normal file
88
product-source/hololake-platform/e2e/quick-open.spec.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Cmd+P opens quick open palette', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open palette with keyboard shortcut
|
||||
await page.keyboard.press('Meta+p')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('.palette')).toBeVisible()
|
||||
await expect(page.locator('.palette__input')).toBeFocused()
|
||||
|
||||
await page.screenshot({ path: 'test-results/quick-open.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('quick open: search and select a note', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Meta+p')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// Type to search
|
||||
await page.fill('.palette__input', 'laputa')
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// Should show matching result
|
||||
await expect(page.locator('.palette__item-title:has-text("Build Laputa App")')).toBeVisible()
|
||||
|
||||
// Press Enter to select
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Palette should close and note should be opened
|
||||
await expect(page.locator('.palette')).not.toBeVisible()
|
||||
// The top result should have been opened (wait for async content load)
|
||||
await expect(page.locator('.editor__tab--active')).toBeVisible({ timeout: 3000 })
|
||||
|
||||
await page.screenshot({ path: 'test-results/quick-open-selected.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('quick open: arrow keys navigate results', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Meta+p')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// First item should be selected by default
|
||||
await expect(page.locator('.palette__item--selected').first()).toBeVisible()
|
||||
|
||||
// Arrow down to move selection
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.waitForTimeout(50)
|
||||
|
||||
// Second item should be selected
|
||||
const items = page.locator('.palette__item')
|
||||
const count = await items.count()
|
||||
expect(count).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
test('quick open: Escape closes palette', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Meta+p')
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.palette')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.palette')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('quick open: clicking outside closes palette', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Meta+p')
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.palette')).toBeVisible()
|
||||
|
||||
// Click the overlay area (outside the palette) using mouse click at top-left
|
||||
await page.mouse.click(10, 10)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.palette')).not.toBeVisible()
|
||||
})
|
||||
80
product-source/hololake-platform/e2e/rename-tab.spec.ts
Normal file
80
product-source/hololake-platform/e2e/rename-tab.spec.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('rename tab by double-clicking', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
// Screenshot initial state
|
||||
await page.screenshot({ path: 'test-results/rename-01-initial.png', fullPage: true })
|
||||
|
||||
// Click a note in the list using text content
|
||||
await page.getByText('Deprecated Workflow').first().click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Screenshot: note opened as tab
|
||||
await page.screenshot({ path: 'test-results/rename-02-note-opened.png', fullPage: true })
|
||||
|
||||
// Find the tab title in the tab bar and double-click it
|
||||
const tabTitle = page.locator('.group span.truncate').first()
|
||||
await expect(tabTitle).toBeVisible({ timeout: 5000 })
|
||||
const originalTitle = await tabTitle.textContent()
|
||||
console.log(`Original title: "${originalTitle}"`)
|
||||
|
||||
await tabTitle.dblclick()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Screenshot: editing mode
|
||||
await page.screenshot({ path: 'test-results/rename-03-editing.png', fullPage: true })
|
||||
|
||||
// Verify input appeared
|
||||
const editInput = page.locator('.group input')
|
||||
await expect(editInput).toBeVisible({ timeout: 3000 })
|
||||
|
||||
// Type new name
|
||||
await editInput.fill('Renamed Test Note')
|
||||
await page.screenshot({ path: 'test-results/rename-04-typing.png', fullPage: true })
|
||||
|
||||
// Press Enter to save
|
||||
await editInput.press('Enter')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Screenshot: after rename
|
||||
await page.screenshot({ path: 'test-results/rename-05-saved.png', fullPage: true })
|
||||
|
||||
// Verify tab title changed
|
||||
const newTabTitle = page.locator('.group span.truncate').first()
|
||||
await expect(newTabTitle).toHaveText('Renamed Test Note')
|
||||
})
|
||||
|
||||
test('cancel rename with Escape', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
// Open a note
|
||||
await page.getByText('Deprecated Workflow').first().click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const tabTitle = page.locator('.group span.truncate').first()
|
||||
const originalTitle = await tabTitle.textContent()
|
||||
|
||||
// Double-click to edit
|
||||
await tabTitle.dblclick()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
const editInput = page.locator('.group input')
|
||||
await expect(editInput).toBeVisible({ timeout: 3000 })
|
||||
|
||||
// Type something different
|
||||
await editInput.fill('Will Be Cancelled')
|
||||
|
||||
// Press Escape to cancel
|
||||
await editInput.press('Escape')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Screenshot: after cancel
|
||||
await page.screenshot({ path: 'test-results/rename-06-cancelled.png', fullPage: true })
|
||||
|
||||
// Verify title unchanged
|
||||
const afterTitle = page.locator('.group span.truncate').first()
|
||||
await expect(afterTitle).toHaveText(originalTitle!)
|
||||
})
|
||||
94
product-source/hololake-platform/e2e/screenshot.spec.ts
Normal file
94
product-source/hololake-platform/e2e/screenshot.spec.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { test } from '@playwright/test'
|
||||
|
||||
test('capture app screenshot for review', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
// Wait for mock data to load
|
||||
await page.waitForTimeout(500)
|
||||
await page.screenshot({ path: 'test-results/app-screenshot.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('capture editor with note selected', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click the first note in the list
|
||||
await page.click('.note-list__item')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
await page.screenshot({ path: 'test-results/editor-screenshot.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('live preview: headings styled, syntax hidden', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click a note to load it
|
||||
await page.click('.note-list__item')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Screenshot showing live preview (headings styled, syntax hidden)
|
||||
await page.screenshot({ path: 'test-results/live-preview.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('tab bar: multiple tabs open and close', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click first note
|
||||
await page.click('.note-list__item')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Debug: screenshot after first click
|
||||
await page.screenshot({ path: 'test-results/tabs-debug-1.png', fullPage: true })
|
||||
|
||||
// Check if other items are visible
|
||||
const items = await page.locator('.note-list__item').count()
|
||||
console.log(`Note list items visible after first click: ${items}`)
|
||||
|
||||
// Click second item if available
|
||||
if (items >= 2) {
|
||||
await page.locator('.note-list__item').nth(1).click({ timeout: 5000 })
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
if (items >= 3) {
|
||||
await page.locator('.note-list__item').nth(2).click({ timeout: 5000 })
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test-results/tabs-screenshot.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('frontmatter hidden from editor view', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click a note that has frontmatter
|
||||
await page.click('.note-list__item')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Frontmatter should be hidden — editor starts with content, not ---
|
||||
await page.screenshot({ path: 'test-results/frontmatter-hidden.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('wikilinks: rendered as styled elements and clickable', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open "Manage Sponsorships" which contains [[Matteo Cellini]] wikilink
|
||||
await page.locator('.note-list__item', { hasText: 'Manage Sponsorships' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Screenshot showing wikilink rendered as styled text
|
||||
await page.screenshot({ path: 'test-results/wikilinks-styled.png', fullPage: true })
|
||||
|
||||
// Click the wikilink to navigate — use mouse.click to fire real mousedown
|
||||
const wikilink = page.locator('.cm-wikilink', { hasText: 'Matteo Cellini' })
|
||||
const box = await wikilink.boundingBox()
|
||||
if (box) {
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2)
|
||||
await page.waitForTimeout(500)
|
||||
// Should now have a new tab for Matteo Cellini
|
||||
await page.screenshot({ path: 'test-results/wikilinks-navigated.png', fullPage: true })
|
||||
}
|
||||
})
|
||||
51
product-source/hololake-platform/e2e/settings-oauth.spec.ts
Normal file
51
product-source/hololake-platform/e2e/settings-oauth.spec.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('settings shows connected GitHub state with username', async ({ page }) => {
|
||||
await page.goto('http://localhost:5243/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open settings
|
||||
await page.keyboard.press('Meta+Comma')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Verify settings panel opened
|
||||
await expect(page.getByTestId('settings-panel')).toBeVisible()
|
||||
|
||||
// Mock data starts with github connected — verify connected state
|
||||
const connected = page.getByTestId('github-connected')
|
||||
await expect(connected).toBeVisible({ timeout: 5000 })
|
||||
await expect(connected).toContainText('lucaong')
|
||||
await expect(connected).toContainText('Connected')
|
||||
|
||||
// Verify disconnect button
|
||||
await expect(page.getByTestId('github-disconnect')).toBeVisible()
|
||||
|
||||
// Verify NO token input field
|
||||
await expect(page.getByTestId('settings-key-github-token')).not.toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/settings-oauth-connected.png' })
|
||||
})
|
||||
|
||||
test('disconnect shows Login with GitHub button', async ({ page }) => {
|
||||
await page.goto('http://localhost:5243/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open settings
|
||||
await page.keyboard.press('Meta+Comma')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click disconnect
|
||||
const disconnectBtn = page.getByTestId('github-disconnect')
|
||||
await expect(disconnectBtn).toBeVisible({ timeout: 5000 })
|
||||
await disconnectBtn.click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Login button should now be visible
|
||||
const loginBtn = page.getByTestId('github-login')
|
||||
await expect(loginBtn).toBeVisible({ timeout: 3000 })
|
||||
await expect(loginBtn).toContainText('Login with GitHub')
|
||||
|
||||
await page.screenshot({ path: 'test-results/settings-oauth-login.png' })
|
||||
})
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
// On macOS, Alt+key produces special characters, so we dispatch events directly
|
||||
function dispatchAltKey(page: import('@playwright/test').Page, key: string) {
|
||||
return page.evaluate((k) => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: k, altKey: true, metaKey: false, ctrlKey: false,
|
||||
bubbles: true, cancelable: true,
|
||||
}))
|
||||
}, key)
|
||||
}
|
||||
|
||||
async function loadApp(page: import('@playwright/test').Page) {
|
||||
await page.goto('/')
|
||||
// Clear stored view mode so we start fresh
|
||||
await page.evaluate(() => localStorage.removeItem('laputa-view-mode'))
|
||||
await page.reload()
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
test.describe('Sidebar collapse', () => {
|
||||
test('default: all three panels visible', async ({ page }) => {
|
||||
await loadApp(page)
|
||||
|
||||
await expect(page.locator('.app__sidebar')).toBeVisible()
|
||||
await expect(page.locator('.app__note-list')).toBeVisible()
|
||||
await expect(page.locator('.app__editor')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/collapse-all-panels.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('collapse button hides sidebar', async ({ page }) => {
|
||||
await loadApp(page)
|
||||
|
||||
const collapseBtn = page.locator('button[aria-label="Collapse sidebar"]')
|
||||
await expect(collapseBtn).toBeVisible()
|
||||
await collapseBtn.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.locator('.app__sidebar')).toHaveCount(0)
|
||||
await expect(page.locator('.app__note-list')).toBeVisible()
|
||||
await expect(page.locator('.app__editor')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/collapse-sidebar-hidden.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('Alt+1 shows editor only', async ({ page }) => {
|
||||
await loadApp(page)
|
||||
|
||||
await dispatchAltKey(page, '1')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.locator('.app__sidebar')).toHaveCount(0)
|
||||
await expect(page.locator('.app__note-list')).toHaveCount(0)
|
||||
await expect(page.locator('.app__editor')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/collapse-editor-only.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('Alt+2 shows editor + note list', async ({ page }) => {
|
||||
await loadApp(page)
|
||||
|
||||
await dispatchAltKey(page, '2')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.locator('.app__sidebar')).toHaveCount(0)
|
||||
await expect(page.locator('.app__note-list')).toBeVisible()
|
||||
await expect(page.locator('.app__editor')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/collapse-editor-list.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('Alt+3 restores all panels after collapse', async ({ page }) => {
|
||||
await loadApp(page)
|
||||
|
||||
// Collapse first
|
||||
await dispatchAltKey(page, '1')
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page.locator('.app__sidebar')).toHaveCount(0)
|
||||
|
||||
// Restore
|
||||
await dispatchAltKey(page, '3')
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page.locator('.app__sidebar')).toBeVisible()
|
||||
await expect(page.locator('.app__note-list')).toBeVisible()
|
||||
await expect(page.locator('.app__editor')).toBeVisible()
|
||||
|
||||
await page.screenshot({ path: 'test-results/collapse-restored.png', fullPage: true })
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Vault Picker Local Options', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('http://localhost:5240')
|
||||
await page.waitForSelector('text=notes', { timeout: 10000 })
|
||||
})
|
||||
|
||||
test('vault menu shows local folder options', async ({ page }) => {
|
||||
// Screenshot before opening menu
|
||||
await page.screenshot({ path: 'test-results/vault-picker-before.png', fullPage: true })
|
||||
|
||||
// Click the vault button in the status bar to open the menu
|
||||
const vaultButton = page.locator('[title="Switch vault"]')
|
||||
await expect(vaultButton).toBeVisible()
|
||||
await vaultButton.click()
|
||||
|
||||
// Wait for menu to appear
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Verify all three options are visible
|
||||
await expect(page.locator('text=Open local folder')).toBeVisible()
|
||||
await expect(page.locator('text=Create new vault')).toBeVisible()
|
||||
await expect(page.locator('text=Connect GitHub repo')).toBeVisible()
|
||||
|
||||
// Screenshot with menu open showing all options
|
||||
await page.screenshot({ path: 'test-results/vault-picker-menu-open.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('vault menu options have correct test IDs', async ({ page }) => {
|
||||
const vaultButton = page.locator('[title="Switch vault"]')
|
||||
await vaultButton.click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('[data-testid="vault-menu-open-local"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="vault-menu-create-new"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="vault-menu-connect-github"]')).toBeVisible()
|
||||
})
|
||||
})
|
||||
103
product-source/hololake-platform/e2e/visual-verify.spec.ts
Normal file
103
product-source/hololake-platform/e2e/visual-verify.spec.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('visual verify: editor theme + list indentation', async ({ page }) => {
|
||||
const consoleErrors: string[] = []
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') consoleErrors.push(msg.text())
|
||||
})
|
||||
|
||||
await page.goto('http://localhost:5173')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Open "Write Weekly Essays" which has bullets, nested items, checkboxes
|
||||
const noteItem = page.locator('.note-list__item', { hasText: 'Write Weekly Essays' })
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
const cmEditor = page.locator('.cm-editor')
|
||||
await expect(cmEditor).toBeVisible()
|
||||
|
||||
// No console errors
|
||||
expect(consoleErrors).toHaveLength(0)
|
||||
|
||||
// --- BUG 2: Verify list indentation ---
|
||||
|
||||
// Level-0 bullets should have 40px padding-left
|
||||
const level0Lines = page.locator('.cm-line.cm-live-list-level-0')
|
||||
const level0Count = await level0Lines.count()
|
||||
console.log(`Level-0 bullet lines: ${level0Count}`)
|
||||
expect(level0Count).toBeGreaterThan(0)
|
||||
|
||||
const paddingL0 = await level0Lines.first().evaluate(el =>
|
||||
window.getComputedStyle(el).paddingLeft
|
||||
)
|
||||
console.log(`Level-0 padding-left: ${paddingL0}`)
|
||||
expect(parseInt(paddingL0, 10)).toBe(40)
|
||||
|
||||
// Bullet widgets and checkboxes are rendered
|
||||
const bulletCount = await page.locator('.cm-live-bullet').count()
|
||||
console.log(`Bullet widgets: ${bulletCount}`)
|
||||
expect(bulletCount).toBeGreaterThan(0)
|
||||
|
||||
const checkboxCount = await page.locator('.cm-live-checkbox').count()
|
||||
console.log(`Checkbox widgets: ${checkboxCount}`)
|
||||
expect(checkboxCount).toBeGreaterThan(0)
|
||||
|
||||
// Screenshot dark mode (top)
|
||||
await page.screenshot({ path: 'test-results/01-dark-mode-editor.png', fullPage: true })
|
||||
|
||||
// Scroll down to see nested items section
|
||||
const scroller = page.locator('.cm-scroller')
|
||||
await scroller.evaluate(el => el.scrollTop = el.scrollHeight)
|
||||
await page.waitForTimeout(300)
|
||||
await page.screenshot({ path: 'test-results/01b-dark-mode-nested.png', fullPage: true })
|
||||
|
||||
// --- BUG 1: Verify theme toggle ---
|
||||
|
||||
// Dark mode: editor bg should be dark
|
||||
const darkBg = await cmEditor.evaluate(el =>
|
||||
window.getComputedStyle(el).backgroundColor
|
||||
)
|
||||
console.log(`Dark mode bg: ${darkBg}`)
|
||||
expect(darkBg).toBe('rgb(15, 15, 26)')
|
||||
|
||||
// Toggle to light mode
|
||||
const themeToggle = page.locator('.sidebar__theme-toggle')
|
||||
await themeToggle.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Scroll back to top for light mode screenshot
|
||||
await scroller.evaluate(el => el.scrollTop = 0)
|
||||
await page.waitForTimeout(200)
|
||||
await page.screenshot({ path: 'test-results/02-light-mode-editor.png', fullPage: true })
|
||||
|
||||
// Light mode: editor bg should be white
|
||||
const lightBg = await cmEditor.evaluate(el =>
|
||||
window.getComputedStyle(el).backgroundColor
|
||||
)
|
||||
console.log(`Light mode bg: ${lightBg}`)
|
||||
expect(lightBg).toBe('rgb(255, 255, 255)')
|
||||
|
||||
// Heading color should be dark in light mode
|
||||
const headingColor = await page.locator('.cm-live-heading').first().evaluate(el =>
|
||||
window.getComputedStyle(el).color
|
||||
)
|
||||
console.log(`Light mode heading color: ${headingColor}`)
|
||||
expect(headingColor).toBe('rgb(55, 53, 47)')
|
||||
|
||||
// Scroll to nested items in light mode
|
||||
await scroller.evaluate(el => el.scrollTop = el.scrollHeight)
|
||||
await page.waitForTimeout(300)
|
||||
await page.screenshot({ path: 'test-results/02b-light-mode-nested.png', fullPage: true })
|
||||
|
||||
// Toggle back to dark mode
|
||||
await themeToggle.click()
|
||||
await page.waitForTimeout(500)
|
||||
await page.screenshot({ path: 'test-results/03-dark-mode-restored.png', fullPage: true })
|
||||
|
||||
const restoredBg = await cmEditor.evaluate(el =>
|
||||
window.getComputedStyle(el).backgroundColor
|
||||
)
|
||||
console.log(`Restored dark mode bg: ${restoredBg}`)
|
||||
expect(restoredBg).toBe('rgb(15, 15, 26)')
|
||||
})
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('closeup: heading marker in gutter', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173')
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
const noteItem = page.locator('.note-list__item', { hasText: 'Build Laputa App' })
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
// Find "Overview" heading and take a tight crop around it
|
||||
const heading = page.locator('.cm-header-2', { hasText: 'Overview' }).first()
|
||||
await expect(heading).toBeVisible()
|
||||
const hBox = await heading.boundingBox()
|
||||
if (!hBox) throw new Error('heading not found')
|
||||
|
||||
// Crop: include gutter area to the left (extra 80px) and a small vertical band
|
||||
const clip = {
|
||||
x: Math.max(0, hBox.x - 80),
|
||||
y: hBox.y - 10,
|
||||
width: hBox.width + 120,
|
||||
height: hBox.height + 20,
|
||||
}
|
||||
|
||||
// Before click — preview mode, no marker visible
|
||||
await page.screenshot({ path: 'test-results/closeup-heading-inactive.png', clip })
|
||||
|
||||
// Click on heading to activate
|
||||
await page.mouse.click(hBox.x + hBox.width / 2, hBox.y + hBox.height / 2)
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// After click — ## marker should appear in gutter
|
||||
await page.screenshot({ path: 'test-results/closeup-heading-active.png', clip })
|
||||
|
||||
// Verify the marker is visible (opacity > 0)
|
||||
const marker = page.locator('.cm-heading-line .cm-formatting-block.cm-formatting-block-visible').first()
|
||||
if (await marker.count() > 0) {
|
||||
const opacity = await marker.evaluate(el => window.getComputedStyle(el).opacity)
|
||||
console.log(`Heading marker opacity when active: ${opacity}`)
|
||||
expect(parseFloat(opacity)).toBeGreaterThan(0)
|
||||
|
||||
const mBox = await marker.boundingBox()
|
||||
console.log(`Heading marker bounding box: ${JSON.stringify(mBox)}`)
|
||||
|
||||
// The marker should be to the LEFT of the heading text
|
||||
if (mBox && hBox) {
|
||||
console.log(`Marker right edge: ${mBox.x + mBox.width}, Heading left edge: ${hBox.x}`)
|
||||
expect(mBox.x + mBox.width).toBeLessThanOrEqual(hBox.x + 5) // marker is left of heading
|
||||
}
|
||||
}
|
||||
|
||||
// Now check bullet closeup
|
||||
const bulletLine = page.locator('.cm-line', { hasText: 'Four-panel layout working' }).first()
|
||||
const bBox = await bulletLine.boundingBox()
|
||||
if (!bBox) throw new Error('bullet not found')
|
||||
|
||||
// First click elsewhere to deactivate
|
||||
await page.mouse.click(hBox.x + 10, hBox.y - 40)
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
const bulletClip = {
|
||||
x: Math.max(0, bBox.x - 20),
|
||||
y: bBox.y - 5,
|
||||
width: Math.min(400, bBox.width + 40),
|
||||
height: bBox.height + 60, // include next line too
|
||||
}
|
||||
|
||||
// Inactive bullets
|
||||
await page.screenshot({ path: 'test-results/closeup-bullet-inactive.png', clip: bulletClip })
|
||||
|
||||
// Click on bullet line
|
||||
await page.mouse.click(bBox.x + bBox.width / 2, bBox.y + bBox.height / 2)
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Active bullet
|
||||
await page.screenshot({ path: 'test-results/closeup-bullet-active.png', clip: bulletClip })
|
||||
})
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('zero-shift detail: focused editor screenshots', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173')
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
// Open "Build Laputa App" which has headings and bullets
|
||||
const noteItem = page.locator('.note-list__item', { hasText: 'Build Laputa App' })
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
const cmEditor = page.locator('.cm-editor')
|
||||
await expect(cmEditor).toBeVisible()
|
||||
|
||||
// Screenshot just the editor content area
|
||||
const editorBox = await cmEditor.boundingBox()
|
||||
if (!editorBox) throw new Error('Editor not visible')
|
||||
|
||||
// Crop to top portion of editor where headings and bullets are
|
||||
const clip = {
|
||||
x: editorBox.x,
|
||||
y: editorBox.y,
|
||||
width: editorBox.width,
|
||||
height: Math.min(editorBox.height, 500),
|
||||
}
|
||||
|
||||
// 1. Initial preview state (cursor not on headings/bullets)
|
||||
await page.screenshot({ path: 'test-results/detail-01-preview.png', clip })
|
||||
|
||||
// 2. Click on "Overview" heading
|
||||
const headingSpan = page.locator('.cm-header-2', { hasText: 'Overview' }).first()
|
||||
const hBox = await headingSpan.boundingBox()
|
||||
if (hBox) {
|
||||
await page.mouse.click(hBox.x + hBox.width / 2, hBox.y + hBox.height / 2)
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
await page.screenshot({ path: 'test-results/detail-02-heading-active.png', clip })
|
||||
|
||||
// 3. Click on a bullet line
|
||||
const bulletLine = page.locator('.cm-line', { hasText: 'Four-panel layout working' }).first()
|
||||
const bBox = await bulletLine.boundingBox()
|
||||
if (bBox) {
|
||||
await page.mouse.click(bBox.x + bBox.width / 2, bBox.y + bBox.height / 2)
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
await page.screenshot({ path: 'test-results/detail-03-bullet-active.png', clip })
|
||||
|
||||
// 4. Click on plain paragraph to go back to preview
|
||||
const para = page.locator('.cm-line', { hasText: 'Custom desktop app' }).first()
|
||||
const pBox = await para.boundingBox()
|
||||
if (pBox) {
|
||||
await page.mouse.click(pBox.x + 10, pBox.y + pBox.height / 2)
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
await page.screenshot({ path: 'test-results/detail-04-back-preview.png', clip })
|
||||
|
||||
// 5. Check that heading marker (.cm-formatting-block inside .cm-heading-line) is position:absolute
|
||||
const headingMarkers = page.locator('.cm-heading-line .cm-formatting-block')
|
||||
const markerCount = await headingMarkers.count()
|
||||
console.log(`Heading markers found: ${markerCount}`)
|
||||
|
||||
if (markerCount > 0) {
|
||||
const position = await headingMarkers.first().evaluate(el =>
|
||||
window.getComputedStyle(el).position
|
||||
)
|
||||
console.log(`Heading marker position: ${position}`)
|
||||
expect(position).toBe('absolute')
|
||||
}
|
||||
|
||||
// 6. Check that non-heading markers always have font-size != 0.01em
|
||||
const bulletMarkers = page.locator('.cm-line:not(.cm-heading-line) .cm-formatting-block')
|
||||
const bmCount = await bulletMarkers.count()
|
||||
console.log(`Non-heading block markers found: ${bmCount}`)
|
||||
|
||||
if (bmCount > 0) {
|
||||
const fontSize = await bulletMarkers.first().evaluate(el =>
|
||||
window.getComputedStyle(el).fontSize
|
||||
)
|
||||
console.log(`Bullet marker font-size: ${fontSize}`)
|
||||
// Should NOT be tiny (library default is 0.01em ≈ 0.15px)
|
||||
expect(parseFloat(fontSize)).toBeGreaterThan(10)
|
||||
}
|
||||
})
|
||||
98
product-source/hololake-platform/e2e/zero-shift.spec.ts
Normal file
98
product-source/hololake-platform/e2e/zero-shift.spec.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('zero horizontal shift: headings and bullets', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173')
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
// Open "Build Laputa App" which has headings and bullets
|
||||
const noteItem = page.locator('.note-list__item', { hasText: 'Build Laputa App' })
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
const cmEditor = page.locator('.cm-editor')
|
||||
await expect(cmEditor).toBeVisible()
|
||||
|
||||
// Screenshot 1: initial state — cursor after frontmatter, headings/bullets in preview mode
|
||||
await page.screenshot({ path: 'test-results/zero-shift-01-initial.png', fullPage: true })
|
||||
|
||||
// Find a heading line (## Overview) and measure its text position
|
||||
const headingText = page.locator('.cm-header-2', { hasText: 'Overview' }).first()
|
||||
await expect(headingText).toBeVisible()
|
||||
|
||||
// Get bounding box BEFORE clicking on it (inactive state)
|
||||
const beforeBox = await headingText.boundingBox()
|
||||
console.log('Heading "Overview" BEFORE click:', JSON.stringify(beforeBox))
|
||||
|
||||
// Screenshot 2: before clicking heading
|
||||
await page.screenshot({ path: 'test-results/zero-shift-02-before-heading-click.png', fullPage: true })
|
||||
|
||||
// Click on the heading to activate it
|
||||
if (beforeBox) {
|
||||
await page.mouse.click(beforeBox.x + beforeBox.width / 2, beforeBox.y + beforeBox.height / 2)
|
||||
}
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Screenshot 3: after clicking heading — ## should appear in gutter, text stays put
|
||||
await page.screenshot({ path: 'test-results/zero-shift-03-after-heading-click.png', fullPage: true })
|
||||
|
||||
// Get bounding box AFTER clicking (active state)
|
||||
const afterBox = await headingText.boundingBox()
|
||||
console.log('Heading "Overview" AFTER click:', JSON.stringify(afterBox))
|
||||
|
||||
// CRITICAL: The heading text X position must not change
|
||||
if (beforeBox && afterBox) {
|
||||
const xShift = Math.abs(afterBox.x - beforeBox.x)
|
||||
console.log(`Horizontal shift: ${xShift}px`)
|
||||
expect(xShift).toBeLessThan(2) // Allow 1px tolerance for subpixel rendering
|
||||
}
|
||||
|
||||
// Now test bullet lines: click on a bullet item
|
||||
const bulletText = page.locator('.cm-line', { hasText: 'Four-panel layout working' }).first()
|
||||
await expect(bulletText).toBeVisible()
|
||||
|
||||
const bulletBeforeBox = await bulletText.boundingBox()
|
||||
console.log('Bullet line BEFORE click:', JSON.stringify(bulletBeforeBox))
|
||||
|
||||
// Screenshot 4: before clicking bullet
|
||||
await page.screenshot({ path: 'test-results/zero-shift-04-before-bullet-click.png', fullPage: true })
|
||||
|
||||
// Click on the bullet line
|
||||
if (bulletBeforeBox) {
|
||||
await page.mouse.click(bulletBeforeBox.x + bulletBeforeBox.width / 2, bulletBeforeBox.y + bulletBeforeBox.height / 2)
|
||||
}
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Screenshot 5: after clicking bullet
|
||||
await page.screenshot({ path: 'test-results/zero-shift-05-after-bullet-click.png', fullPage: true })
|
||||
|
||||
const bulletAfterBox = await bulletText.boundingBox()
|
||||
console.log('Bullet line AFTER click:', JSON.stringify(bulletAfterBox))
|
||||
|
||||
// CRITICAL: Bullet line X position must not change
|
||||
if (bulletBeforeBox && bulletAfterBox) {
|
||||
const xShift = Math.abs(bulletAfterBox.x - bulletBeforeBox.x)
|
||||
console.log(`Bullet horizontal shift: ${xShift}px`)
|
||||
expect(xShift).toBeLessThan(2)
|
||||
}
|
||||
|
||||
// Screenshot 6: click somewhere else (a plain paragraph) to verify heading/bullets go back to preview
|
||||
const paragraph = page.locator('.cm-line', { hasText: 'Custom desktop app' }).first()
|
||||
if (await paragraph.isVisible()) {
|
||||
const paraBox = await paragraph.boundingBox()
|
||||
if (paraBox) {
|
||||
await page.mouse.click(paraBox.x + 10, paraBox.y + paraBox.height / 2)
|
||||
}
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
await page.screenshot({ path: 'test-results/zero-shift-06-back-to-preview.png', fullPage: true })
|
||||
|
||||
// Verify heading underline is removed (FIX 1)
|
||||
const headingLine = page.locator('.cm-heading-line').first()
|
||||
if (await headingLine.count() > 0) {
|
||||
const borderBottom = await headingLine.evaluate(el =>
|
||||
window.getComputedStyle(el).borderBottom
|
||||
)
|
||||
console.log(`Heading line border-bottom: ${borderBottom}`)
|
||||
expect(borderBottom).toContain('none')
|
||||
}
|
||||
})
|
||||
Loading…
Reference in a new issue