diff --git a/package.json b/package.json index 7634ac53..81507c67 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "vitest run", "test:watch": "vitest", "test:e2e": "playwright test", - "playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/raw-editor-sync-to-blocknote.spec.ts tests/integration/vault-workflows.spec.ts", + "playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/create-note-crash.spec.ts tests/smoke/raw-editor-sync-to-blocknote.spec.ts tests/smoke/rename-wikilink-update.spec.ts", "playwright:regression": "playwright test tests/smoke/", "playwright:integration": "playwright test --config playwright.integration.config.ts", "test:coverage": "vitest run --coverage", diff --git a/tests/helpers/fixtureVault.ts b/tests/helpers/fixtureVault.ts new file mode 100644 index 00000000..b4fe650d --- /dev/null +++ b/tests/helpers/fixtureVault.ts @@ -0,0 +1,82 @@ +import { expect, type Page } from '@playwright/test' +import fs from 'fs' +import os from 'os' +import path from 'path' + +const FIXTURE_VAULT = path.resolve('tests/fixtures/test-vault') + +function copyDirSync(src: string, dest: string): void { + fs.mkdirSync(dest, { recursive: true }) + for (const item of fs.readdirSync(src, { withFileTypes: true })) { + const sourcePath = path.join(src, item.name) + const destinationPath = path.join(dest, item.name) + if (item.isDirectory()) { + copyDirSync(sourcePath, destinationPath) + continue + } + fs.copyFileSync(sourcePath, destinationPath) + } +} + +export function createFixtureVaultCopy(): string { + const tempVaultDir = fs.mkdtempSync(path.join(os.tmpdir(), 'laputa-test-vault-')) + copyDirSync(FIXTURE_VAULT, tempVaultDir) + return tempVaultDir +} + +export function removeFixtureVaultCopy(tempVaultDir: string | null | undefined): void { + if (!tempVaultDir) return + fs.rmSync(tempVaultDir, { recursive: true, force: true }) +} + +export async function openFixtureVault( + page: Page, + vaultPath: string, +): Promise { + await page.addInitScript((resolvedVaultPath: string) => { + localStorage.clear() + + const nativeFetch = window.fetch.bind(window) + window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { + const requestUrl = typeof input === 'string' + ? input + : input instanceof Request + ? input.url + : input.toString() + + if (requestUrl.endsWith('/api/vault/ping') || requestUrl.includes('/api/vault/ping?')) { + return Promise.resolve(new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + })) + } + + return nativeFetch(input, init) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let ref: any = null + Object.defineProperty(window, '__mockHandlers', { + configurable: true, + set(value) { + ref = value + ref.load_vault_list = () => ({ + vaults: [{ label: 'Test Vault', path: resolvedVaultPath }], + active_vault: resolvedVaultPath, + hidden_defaults: [], + }) + ref.check_vault_exists = () => true + ref.get_last_vault_path = () => resolvedVaultPath + ref.get_default_vault_path = () => resolvedVaultPath + ref.save_vault_list = () => null + }, + get() { + return ref + }, + }) + }, vaultPath) + + await page.goto('/') + await page.locator('[data-testid="note-list-container"]').waitFor({ timeout: 15_000 }) + await expect(page.getByText('Alpha Project', { exact: true }).first()).toBeVisible({ timeout: 15_000 }) +} diff --git a/tests/integration/vault-workflows.spec.ts b/tests/integration/vault-workflows.spec.ts index 12f1e4e2..8501139c 100644 --- a/tests/integration/vault-workflows.spec.ts +++ b/tests/integration/vault-workflows.spec.ts @@ -8,58 +8,18 @@ import { test, expect } from '@playwright/test' import fs from 'fs' import path from 'path' -import os from 'os' - -const FIXTURE_VAULT = path.resolve('tests/fixtures/test-vault') - -function copyDirSync(src: string, dest: string): void { - fs.mkdirSync(dest, { recursive: true }) - for (const item of fs.readdirSync(src, { withFileTypes: true })) { - const s = path.join(src, item.name) - const d = path.join(dest, item.name) - if (item.isDirectory()) copyDirSync(s, d) - else fs.copyFileSync(s, d) - } -} +import { createFixtureVaultCopy, openFixtureVault, removeFixtureVaultCopy } from '../helpers/fixtureVault' let tempVaultDir: string -test.beforeEach(async ({ page }) => { - // Fresh vault copy for each test - tempVaultDir = fs.mkdtempSync(path.join(os.tmpdir(), 'laputa-test-vault-')) - copyDirSync(FIXTURE_VAULT, tempVaultDir) - - // Intercept window.__mockHandlers assignment to override vault path handlers. - // The Object.defineProperty setter fires when mock-tauri/index.ts sets - // window.__mockHandlers = mockHandlers, letting us patch the same object - // reference that mockInvoke reads from. - await page.addInitScript((vaultPath: string) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let ref: any = null - Object.defineProperty(window, '__mockHandlers', { - set(val) { - ref = val - ref.load_vault_list = () => ({ - vaults: [{ label: 'Test Vault', path: vaultPath }], - active_vault: vaultPath, - }) - ref.check_vault_exists = () => true - ref.get_last_vault_path = () => vaultPath - ref.get_default_vault_path = () => vaultPath - ref.save_vault_list = () => null - }, - get() { return ref }, - configurable: true, - }) - }, tempVaultDir) - - await page.goto('/') - // Wait until the real vault entries are loaded in the sidebar - await page.getByText('Alpha Project', { exact: true }).first().waitFor({ timeout: 10_000 }) +test.beforeEach(async ({ page }, testInfo) => { + testInfo.setTimeout(60_000) + tempVaultDir = createFixtureVaultCopy() + await openFixtureVault(page, tempVaultDir) }) test.afterEach(async () => { - fs.rmSync(tempVaultDir, { recursive: true, force: true }) + removeFixtureVaultCopy(tempVaultDir) }) /** Helper: locate the note-list container. */ diff --git a/tests/smoke/create-note-crash.spec.ts b/tests/smoke/create-note-crash.spec.ts index cd96eac8..c33c40de 100644 --- a/tests/smoke/create-note-crash.spec.ts +++ b/tests/smoke/create-note-crash.spec.ts @@ -5,7 +5,7 @@ function isCrashError(msg: string): boolean { return msg.includes('Maximum update depth') || msg.includes('Invalid hook call') || msg.includes('#185') } -test('create note via Cmd+N does not crash', async ({ page }) => { +test('create note via Cmd+N does not crash @smoke', async ({ page }) => { const errors: string[] = [] page.on('pageerror', (err) => { if (isCrashError(err.message)) errors.push(err.message) }) @@ -17,7 +17,9 @@ test('create note via Cmd+N does not crash', async ({ page }) => { await page.waitForTimeout(2000) expect(errors).toHaveLength(0) - await expect(page.locator('[data-testid="title-field-input"]')).toBeVisible() + const noteList = page.locator('[data-testid="note-list-container"]') + await expect(noteList.getByText(/Untitled Note/i).first()).toBeVisible({ timeout: 5000 }) + await expect(page.getByRole('textbox').last()).toBeVisible() }) test('create note via sidebar + button does not crash', async ({ page }) => { diff --git a/tests/smoke/h1-title-decoupled.spec.ts b/tests/smoke/h1-title-decoupled.spec.ts index 57822f97..8eabba59 100644 --- a/tests/smoke/h1-title-decoupled.spec.ts +++ b/tests/smoke/h1-title-decoupled.spec.ts @@ -1,52 +1,16 @@ import { test, expect } from '@playwright/test' -import fs from 'fs' -import path from 'path' -import os from 'os' - -const FIXTURE_VAULT = path.resolve('tests/fixtures/test-vault') - -function copyDirSync(src: string, dest: string): void { - fs.mkdirSync(dest, { recursive: true }) - for (const item of fs.readdirSync(src, { withFileTypes: true })) { - const s = path.join(src, item.name) - const d = path.join(dest, item.name) - if (item.isDirectory()) copyDirSync(s, d) - else fs.copyFileSync(s, d) - } -} +import { createFixtureVaultCopy, openFixtureVault, removeFixtureVaultCopy } from '../helpers/fixtureVault' let tempVaultDir: string -test.beforeEach(async ({ page }) => { - tempVaultDir = fs.mkdtempSync(path.join(os.tmpdir(), 'laputa-test-vault-')) - copyDirSync(FIXTURE_VAULT, tempVaultDir) - - await page.addInitScript((vaultPath: string) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let ref: any = null - Object.defineProperty(window, '__mockHandlers', { - set(val) { - ref = val - ref.load_vault_list = () => ({ - vaults: [{ label: 'Test Vault', path: vaultPath }], - active_vault: vaultPath, - }) - ref.check_vault_exists = () => true - ref.get_last_vault_path = () => vaultPath - ref.get_default_vault_path = () => vaultPath - ref.save_vault_list = () => null - }, - get() { return ref }, - configurable: true, - }) - }, tempVaultDir) - - await page.goto('/') - await page.getByText('Alpha Project', { exact: true }).first().waitFor({ timeout: 10_000 }) +test.beforeEach(async ({ page }, testInfo) => { + testInfo.setTimeout(60_000) + tempVaultDir = createFixtureVaultCopy() + await openFixtureVault(page, tempVaultDir) }) test.afterEach(async () => { - fs.rmSync(tempVaultDir, { recursive: true, force: true }) + removeFixtureVaultCopy(tempVaultDir) }) test('creating an untitled draft hides the legacy title section in the editor', async ({ page }) => { diff --git a/tests/smoke/rename-wikilink-update.spec.ts b/tests/smoke/rename-wikilink-update.spec.ts index f65cded3..c851ad77 100644 --- a/tests/smoke/rename-wikilink-update.spec.ts +++ b/tests/smoke/rename-wikilink-update.spec.ts @@ -7,7 +7,7 @@ test.describe('Renaming a note updates wikilinks across the vault', () => { await page.waitForLoadState('networkidle') }) - test('title field rename triggers rename flow and shows toast', async ({ page }) => { + test('title field rename triggers rename flow and shows toast @smoke', async ({ page }) => { // 1. Click the first note in the list to open it const noteListContainer = page.locator('[data-testid="note-list-container"]') await noteListContainer.waitFor({ timeout: 5000 })