From b4a1c63ed81bbe27098b67c2c42ed538e839d372 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Tue, 30 Dec 2025 21:22:51 +1100 Subject: [PATCH] chore: add E2E test scripts and Playwright configuration - Add e2e-setup.sh for downloading and unpacking Obsidian AppImage - Add e2e-launch.sh for launching Obsidian with test vault - Add playwright.config.ts with Electron test configuration - Add initial E2E test structure in e2e/ directory --- e2e-launch.sh | 26 +++ e2e-setup.sh | 101 +++++++++ e2e/obsidian.ts | 140 ++++++++++++ e2e/tasknotes.spec.ts | 501 ++++++++++++++++++++++++++++++++++++++++++ playwright.config.ts | 22 ++ 5 files changed, 790 insertions(+) create mode 100755 e2e-launch.sh create mode 100755 e2e-setup.sh create mode 100644 e2e/obsidian.ts create mode 100644 e2e/tasknotes.spec.ts create mode 100644 playwright.config.ts diff --git a/e2e-launch.sh b/e2e-launch.sh new file mode 100755 index 00000000..7d4425cd --- /dev/null +++ b/e2e-launch.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -eu -o pipefail + +# Launch Obsidian with the e2e test vault for manual setup/testing + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UNPACKED_DIR="$SCRIPT_DIR/.obsidian-unpacked" +E2E_VAULT_DIR="$SCRIPT_DIR/tasknotes-e2e-vault" + +if [[ ! -x "$UNPACKED_DIR/obsidian" ]]; then + echo "Error: Unpacked Obsidian not found. Run e2e-setup.sh first." + exit 1 +fi + +if [[ ! -d "$E2E_VAULT_DIR" ]]; then + echo "Error: E2E vault not found at $E2E_VAULT_DIR" + exit 1 +fi + +echo "Launching Obsidian with e2e vault..." +echo "Vault: $E2E_VAULT_DIR" + +# Launch the unpacked Obsidian binary with the test vault +# Pass the vault path directly as an argument +cd "$UNPACKED_DIR" +./obsidian --no-sandbox "$E2E_VAULT_DIR" diff --git a/e2e-setup.sh b/e2e-setup.sh new file mode 100755 index 00000000..ef8d21c7 --- /dev/null +++ b/e2e-setup.sh @@ -0,0 +1,101 @@ +#!/bin/bash +set -eu -o pipefail + +# E2E Test Setup Script for Linux +# Adapted from biblib for tasknotes + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OBSIDIAN_APPIMAGE="${1:-$HOME/Applications/Obsidian-1.8.10.AppImage}" +OBSIDIAN_DATA_DIR="$HOME/.config/obsidian" +UNPACKED_DIR="$SCRIPT_DIR/.obsidian-unpacked" +E2E_VAULT_DIR="$SCRIPT_DIR/tasknotes-e2e-vault" +PLUGIN_DIR="$E2E_VAULT_DIR/.obsidian/plugins/tasknotes" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +echo_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Check prerequisites +if [[ ! -f "$OBSIDIAN_APPIMAGE" ]]; then + echo_error "Obsidian AppImage not found at: $OBSIDIAN_APPIMAGE" + echo "Usage: $0 [path-to-obsidian-appimage]" + exit 1 +fi + +if [[ ! -d "$E2E_VAULT_DIR" ]]; then + echo_error "E2E vault not found at: $E2E_VAULT_DIR" + echo "Please create the e2e-vault directory first." + exit 1 +fi + +# Find the latest obsidian.asar in the data directory +OBSIDIAN_ASAR=$(ls -t "$OBSIDIAN_DATA_DIR"/obsidian-*.asar 2>/dev/null | head -1) +if [[ -z "$OBSIDIAN_ASAR" ]]; then + echo_error "No obsidian.asar found in $OBSIDIAN_DATA_DIR" + exit 1 +fi +echo_info "Using Obsidian ASAR: $OBSIDIAN_ASAR" + +# Step 1: Extract AppImage to get Electron +echo_info "Extracting Obsidian AppImage..." +TEMP_EXTRACT="/tmp/obsidian-appimage-extract-$$" +rm -rf "$TEMP_EXTRACT" +cd /tmp +"$OBSIDIAN_APPIMAGE" --appimage-extract > /dev/null 2>&1 +mv squashfs-root "$TEMP_EXTRACT" + +# Step 2: Unpack the ASAR +echo_info "Unpacking Obsidian ASAR..." +rm -rf "$UNPACKED_DIR" +mkdir -p "$UNPACKED_DIR" + +# Copy Electron files from extracted AppImage +cp -r "$TEMP_EXTRACT"/* "$UNPACKED_DIR/" + +# Unpack the main obsidian.asar +npx @electron/asar extract "$OBSIDIAN_ASAR" "$UNPACKED_DIR/resources/app" + +# Also extract the app.asar loader if it exists +if [[ -f "$UNPACKED_DIR/resources/app.asar" ]]; then + npx @electron/asar extract "$UNPACKED_DIR/resources/app.asar" "$UNPACKED_DIR/resources/app-loader" +fi + +# Clean up temp extraction +rm -rf "$TEMP_EXTRACT" + +# Step 3: Build the plugin +echo_info "Building the plugin..." +cd "$SCRIPT_DIR" +npm run build + +# Step 4: Set up symlinks in the test vault +echo_info "Setting up plugin symlinks in test vault..." +mkdir -p "$PLUGIN_DIR" + +# Remove existing symlinks/files +rm -f "$PLUGIN_DIR/main.js" "$PLUGIN_DIR/manifest.json" "$PLUGIN_DIR/styles.css" + +# Create symlinks +ln -sf "$SCRIPT_DIR/main.js" "$PLUGIN_DIR/main.js" +ln -sf "$SCRIPT_DIR/manifest.json" "$PLUGIN_DIR/manifest.json" +if [[ -f "$SCRIPT_DIR/styles.css" ]]; then + ln -sf "$SCRIPT_DIR/styles.css" "$PLUGIN_DIR/styles.css" +fi + +echo_info "Setup complete!" +echo "" +echo "Next steps:" +echo " 1. Run: npm run e2e:launch" +echo " This will open Obsidian with the test vault." +echo " 2. Enable community plugins in Settings > Community plugins" +echo " 3. Enable the 'TaskNotes' plugin" +echo " 4. Close Obsidian" +echo " 5. Run: npm run e2e" +echo "" +echo_warn "Note: First-time setup requires manual plugin activation." diff --git a/e2e/obsidian.ts b/e2e/obsidian.ts new file mode 100644 index 00000000..9d6dffdb --- /dev/null +++ b/e2e/obsidian.ts @@ -0,0 +1,140 @@ +import { Page, chromium, Browser } from '@playwright/test'; +import * as path from 'path'; +import * as fs from 'fs'; +import { spawn, ChildProcess } from 'child_process'; + +const PROJECT_ROOT = path.resolve(__dirname, '..'); +const UNPACKED_DIR = path.join(PROJECT_ROOT, '.obsidian-unpacked'); +const E2E_VAULT_DIR = path.join(PROJECT_ROOT, 'tasknotes-e2e-vault'); + +export interface ObsidianApp { + browser?: Browser; + process?: ChildProcess; + page: Page; +} + +export async function launchObsidian(): Promise { + // Check that setup has been run + const obsidianBinary = path.join(UNPACKED_DIR, 'obsidian'); + if (!fs.existsSync(obsidianBinary)) { + throw new Error( + 'Obsidian unpacked directory not found. Run `npm run e2e:setup` first.' + ); + } + + // Launch Obsidian manually and connect via CDP + const remoteDebuggingPort = 9222; + + // Pass the vault path directly as an argument + const obsidianProcess = spawn(obsidianBinary, [ + '--no-sandbox', + `--remote-debugging-port=${remoteDebuggingPort}`, + E2E_VAULT_DIR, + ], { + cwd: UNPACKED_DIR, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + // Wait for DevTools to be ready + let cdpUrl = ''; + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timeout waiting for DevTools')), 30000); + + obsidianProcess.stderr?.on('data', (data: Buffer) => { + const output = data.toString(); + const match = output.match(/DevTools listening on (ws:\/\/[^\s]+)/); + if (match) { + cdpUrl = match[1]; + clearTimeout(timeout); + resolve(); + } + }); + + obsidianProcess.on('error', (err) => { + clearTimeout(timeout); + reject(err); + }); + }); + + console.log('Connecting to CDP:', cdpUrl); + + // Connect to Obsidian via CDP + const browser = await chromium.connectOverCDP(cdpUrl); + + // Get the first page/context + let contexts = browser.contexts(); + let page: Page; + + if (contexts.length > 0 && contexts[0].pages().length > 0) { + page = contexts[0].pages()[0]; + } else { + // Wait for a context and page to be created + if (contexts.length === 0) { + await new Promise((resolve) => { + const checkContexts = () => { + contexts = browser.contexts(); + if (contexts.length > 0) { + resolve(); + } else { + setTimeout(checkContexts, 100); + } + }; + checkContexts(); + }); + } + const context = contexts[0]; + if (context.pages().length > 0) { + page = context.pages()[0]; + } else { + page = await context.waitForEvent('page'); + } + } + + // Wait for Obsidian to fully load + await page.waitForLoadState('domcontentloaded'); + + // Give Obsidian time to initialize + await page.waitForTimeout(3000); + + // Handle "Trust this vault" dialog + const trustButton = page.locator('button:has-text("Trust")'); + if (await trustButton.isVisible({ timeout: 2000 }).catch(() => false)) { + await trustButton.click(); + await page.waitForTimeout(1000); + } + + // Wait for the workspace to be ready + await page.waitForSelector('.workspace', { timeout: 30000 }); + + return { browser, process: obsidianProcess, page }; +} + +export async function closeObsidian(app: ObsidianApp): Promise { + if (app.browser) { + await app.browser.close(); + } + if (app.process) { + app.process.kill(); + } +} + +export async function openCommandPalette(page: Page): Promise { + // Use Ctrl+P to open command palette + await page.keyboard.press('Control+p'); + await page.waitForSelector('.prompt', { timeout: 5000 }); +} + +export async function runCommand(page: Page, command: string): Promise { + await openCommandPalette(page); + await page.keyboard.type(command, { delay: 50 }); + await page.waitForTimeout(300); // Wait for search to filter + await page.keyboard.press('Enter'); +} + +export async function waitForNotice(page: Page, text?: string): Promise { + const notice = page.locator('.notice'); + await notice.waitFor({ timeout: 10000 }); + if (text) { + await notice.filter({ hasText: text }).waitFor({ timeout: 5000 }); + } +} diff --git a/e2e/tasknotes.spec.ts b/e2e/tasknotes.spec.ts new file mode 100644 index 00000000..c4402959 --- /dev/null +++ b/e2e/tasknotes.spec.ts @@ -0,0 +1,501 @@ +import { test, expect } from '@playwright/test'; +import { launchObsidian, closeObsidian, ObsidianApp, runCommand } from './obsidian'; + +let app: ObsidianApp; + +test.beforeAll(async () => { + app = await launchObsidian(); +}); + +test.afterAll(async () => { + if (app) { + await closeObsidian(app); + } +}); + +test.describe('TaskNotes Plugin', () => { + test('should load and show commands in command palette', async () => { + const { page } = app; + + // Open command palette with Ctrl+P + await page.keyboard.press('Control+p'); + await page.waitForSelector('.prompt', { timeout: 5000 }); + + // Search for TaskNotes commands + await page.keyboard.type('tasknotes', { delay: 50 }); + await page.waitForTimeout(500); + + // Verify that TaskNotes commands appear + const suggestions = page.locator('.suggestion-item'); + await expect(suggestions.first()).toBeVisible(); + + // Screenshot: command palette with TaskNotes commands + await page.screenshot({ path: 'test-results/screenshots/command-palette-tasknotes.png' }); + + // Check for expected commands + const suggestionText = await page.locator('.prompt-results').textContent(); + expect(suggestionText).toContain('TaskNotes'); + + // Close command palette + await page.keyboard.press('Escape'); + await page.waitForTimeout(300); + }); + + test('should open calendar view via command', async () => { + const { page } = app; + + // Open command palette + await page.keyboard.press('Control+p'); + await page.waitForSelector('.prompt', { timeout: 5000 }); + + // Search for the calendar command + await page.keyboard.type('calendar', { delay: 30 }); + await page.waitForTimeout(500); + + // Verify suggestion is visible then press Enter to execute + const suggestion = page.locator('.suggestion-item').first(); + await expect(suggestion).toBeVisible(); + + // Press Enter to execute the command + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + + // Screenshot: after running calendar command + await page.screenshot({ path: 'test-results/screenshots/calendar-view.png' }); + + // Verify the calendar view is visible (FullCalendar container) + const calendarContainer = page.locator('.fc'); + await expect(calendarContainer).toBeVisible({ timeout: 10000 }); + }); + + test('should create a new task via command', async () => { + const { page } = app; + + // Open command palette + await page.keyboard.press('Control+p'); + await page.waitForSelector('.prompt', { timeout: 5000 }); + + // Search for the create task command + await page.keyboard.type('Create new task', { delay: 30 }); + await page.waitForTimeout(500); + + // Verify suggestion is visible + const suggestion = page.locator('.suggestion-item').first(); + await expect(suggestion).toBeVisible(); + + // Press Enter to execute the command + await page.keyboard.press('Enter'); + await page.waitForTimeout(500); + + // Screenshot: task creation modal or input + await page.screenshot({ path: 'test-results/screenshots/create-task.png' }); + + // Close any modal that opened + await page.keyboard.press('Escape'); + await page.waitForTimeout(300); + }); +}); + +test.describe('TaskNotes Views', () => { + test('should open kanban board via sidebar', async () => { + const { page } = app; + + // First expand the Views folder if it's collapsed + const viewsFolder = page.locator('.nav-folder-title:has-text("Views")'); + if (await viewsFolder.isVisible()) { + // Check if folder is collapsed (has is-collapsed class on parent) + const isCollapsed = await viewsFolder.locator('..').evaluate(el => el.classList.contains('is-collapsed')); + if (isCollapsed) { + await viewsFolder.click(); + await page.waitForTimeout(300); + } + } + + // Click on kanban-default in the sidebar to open it + const kanbanItem = page.locator('.nav-file-title:has-text("kanban-default")'); + await kanbanItem.click(); + await page.waitForTimeout(1500); + + await page.screenshot({ path: 'test-results/screenshots/kanban-view.png' }); + + // Verify a Bases view container is visible (may be calendar or kanban depending on config) + const viewContainer = page.locator('.tn-bases-integration, .fc, .kanban-view__board').first(); + await expect(viewContainer).toBeVisible({ timeout: 10000 }); + }); + + test('should open tasks view via sidebar', async () => { + const { page } = app; + + // First expand the Views folder if it's collapsed + const viewsFolder = page.locator('.nav-folder-title:has-text("Views")'); + if (await viewsFolder.isVisible()) { + const isCollapsed = await viewsFolder.locator('..').evaluate(el => el.classList.contains('is-collapsed')); + if (isCollapsed) { + await viewsFolder.click(); + await page.waitForTimeout(300); + } + } + + // Click on tasks-default in the sidebar to open it + const tasksItem = page.locator('.nav-file-title:has-text("tasks-default")'); + await tasksItem.click(); + await page.waitForTimeout(1500); + + await page.screenshot({ path: 'test-results/screenshots/tasks-view.png' }); + + // Verify tasks view elements - uses tn-bases-integration container + const tasksContainer = page.locator('.tn-bases-integration, .tn-tasklist').first(); + await expect(tasksContainer).toBeVisible({ timeout: 10000 }); + }); + + test('should open mini calendar view via sidebar', async () => { + const { page } = app; + + // First expand the Views folder if it's collapsed + const viewsFolder = page.locator('.nav-folder-title:has-text("Views")'); + if (await viewsFolder.isVisible()) { + const isCollapsed = await viewsFolder.locator('..').evaluate(el => el.classList.contains('is-collapsed')); + if (isCollapsed) { + await viewsFolder.click(); + await page.waitForTimeout(300); + } + } + + // Click on mini-calendar-default in the sidebar + const miniCalItem = page.locator('.nav-file-title:has-text("mini-calendar-default")'); + await miniCalItem.click(); + await page.waitForTimeout(1500); + + await page.screenshot({ path: 'test-results/screenshots/mini-calendar-view.png' }); + }); + + test('should open agenda view via sidebar', async () => { + const { page } = app; + + // First expand the Views folder if it's collapsed + const viewsFolder = page.locator('.nav-folder-title:has-text("Views")'); + if (await viewsFolder.isVisible()) { + const isCollapsed = await viewsFolder.locator('..').evaluate(el => el.classList.contains('is-collapsed')); + if (isCollapsed) { + await viewsFolder.click(); + await page.waitForTimeout(300); + } + } + + // Click on agenda-default in the sidebar + const agendaItem = page.locator('.nav-file-title:has-text("agenda-default")'); + await agendaItem.click(); + await page.waitForTimeout(1500); + + await page.screenshot({ path: 'test-results/screenshots/agenda-view.png' }); + + // Verify agenda view loads (uses FullCalendar's listWeek view or shows error) + const agendaContainer = page.locator('.fc, .tn-bases-integration, .tn-bases-error').first(); + await expect(agendaContainer).toBeVisible({ timeout: 10000 }); + }); +}); + +test.describe('Calendar View Modes', () => { + test('should switch to week view', async () => { + const { page } = app; + + // First ensure calendar is open + await runCommand(page, 'Open calendar view'); + await page.waitForTimeout(500); + + // Click the Week button (W) + const weekButton = page.locator('.tn-view-toolbar button:has-text("W")'); + if (await weekButton.isVisible()) { + await weekButton.click(); + await page.waitForTimeout(500); + } + + await page.screenshot({ path: 'test-results/screenshots/calendar-week-view.png' }); + }); + + test('should switch to day view', async () => { + const { page } = app; + + // Click the Day button (D) + const dayButton = page.locator('.tn-view-toolbar button:has-text("D")'); + if (await dayButton.isVisible()) { + await dayButton.click(); + await page.waitForTimeout(500); + } + + await page.screenshot({ path: 'test-results/screenshots/calendar-day-view.png' }); + }); + + test('should switch to year view', async () => { + const { page } = app; + + // Click the Year button (Y) + const yearButton = page.locator('.tn-view-toolbar button:has-text("Y")'); + if (await yearButton.isVisible()) { + await yearButton.click(); + await page.waitForTimeout(500); + } + + await page.screenshot({ path: 'test-results/screenshots/calendar-year-view.png' }); + }); + + test('should switch to list view', async () => { + const { page } = app; + + // Click the List button (L) + const listButton = page.locator('.tn-view-toolbar button:has-text("L")'); + if (await listButton.isVisible()) { + await listButton.click(); + await page.waitForTimeout(500); + } + + await page.screenshot({ path: 'test-results/screenshots/calendar-list-view.png' }); + }); + + test('should switch back to month view', async () => { + const { page } = app; + + // Click the Month button (M) + const monthButton = page.locator('.tn-view-toolbar button:has-text("M")'); + if (await monthButton.isVisible()) { + await monthButton.click(); + await page.waitForTimeout(500); + } + + await page.screenshot({ path: 'test-results/screenshots/calendar-month-view.png' }); + }); +}); + +test.describe('Pomodoro Timer', () => { + test('should open pomodoro timer', async () => { + const { page } = app; + + await runCommand(page, 'Open pomodoro timer'); + await page.waitForTimeout(1000); + + await page.screenshot({ path: 'test-results/screenshots/pomodoro-timer.png' }); + + // Look for pomodoro elements - uses pomodoro-view container + const pomodoroView = page.locator('.pomodoro-view'); + await expect(pomodoroView).toBeVisible({ timeout: 10000 }); + }); + + test('should open pomodoro statistics', async () => { + const { page } = app; + + await runCommand(page, 'Open pomodoro statistics'); + await page.waitForTimeout(1000); + + await page.screenshot({ path: 'test-results/screenshots/pomodoro-statistics.png' }); + }); +}); + +test.describe('Task Creation Modal', () => { + test('should explore task modal fields', async () => { + const { page } = app; + + await runCommand(page, 'Create new task'); + await page.waitForTimeout(500); + + // Verify modal is open + const modal = page.locator('.modal'); + await expect(modal).toBeVisible(); + + // Screenshot the initial modal state + await page.screenshot({ path: 'test-results/screenshots/task-modal-initial.png' }); + + // Try clicking on the status dropdown + const statusDropdown = modal.locator('.tn-status-dropdown, [class*="status"]').first(); + if (await statusDropdown.isVisible()) { + await statusDropdown.click(); + await page.waitForTimeout(300); + await page.screenshot({ path: 'test-results/screenshots/task-modal-status-dropdown.png' }); + await page.keyboard.press('Escape'); + await page.waitForTimeout(200); + } + + // Try clicking on the date picker icon + const dateIcon = modal.locator('[aria-label*="date"], [class*="calendar-icon"], svg').first(); + if (await dateIcon.isVisible()) { + await dateIcon.click(); + await page.waitForTimeout(300); + await page.screenshot({ path: 'test-results/screenshots/task-modal-date-picker.png' }); + } + + // Close modal + await page.keyboard.press('Escape'); + await page.waitForTimeout(300); + }); +}); + +test.describe('Properties Panel', () => { + test('should open properties panel', async () => { + const { page } = app; + + // Open calendar view first + await runCommand(page, 'Open calendar view'); + await page.waitForTimeout(500); + + // Click the Properties button in toolbar + const propertiesButton = page.locator('button:has-text("Properties"), [aria-label*="Properties"]'); + if (await propertiesButton.isVisible()) { + await propertiesButton.click(); + await page.waitForTimeout(500); + await page.screenshot({ path: 'test-results/screenshots/properties-panel.png' }); + } + }); + + test('should open filter panel', async () => { + const { page } = app; + + // Click the Filter button in toolbar + const filterButton = page.locator('button:has-text("Filter"), [aria-label*="Filter"]'); + if (await filterButton.isVisible()) { + await filterButton.click(); + await page.waitForTimeout(500); + await page.screenshot({ path: 'test-results/screenshots/filter-panel.png' }); + } + }); + + test('should open sort options', async () => { + const { page } = app; + + // Click the Sort button in toolbar + const sortButton = page.locator('button:has-text("Sort"), [aria-label*="Sort"]'); + if (await sortButton.isVisible()) { + await sortButton.click(); + await page.waitForTimeout(500); + await page.screenshot({ path: 'test-results/screenshots/sort-options.png' }); + await page.keyboard.press('Escape'); + } + }); +}); + +test.describe('Statistics Views', () => { + test('should open task and project statistics', async () => { + const { page } = app; + + await runCommand(page, 'Open task & project statistics'); + await page.waitForTimeout(1000); + + await page.screenshot({ path: 'test-results/screenshots/task-statistics.png' }); + }); +}); + +test.describe('Sidebar Navigation', () => { + test('should show TaskNotes sidebar items', async () => { + const { page } = app; + + // Screenshot the sidebar showing TaskNotes tree + const sidebar = page.locator('.workspace-split.mod-left-split'); + await page.screenshot({ path: 'test-results/screenshots/sidebar-navigation.png' }); + + // Try clicking different sidebar items + const viewsFolder = page.locator('.nav-folder-title:has-text("Views")'); + if (await viewsFolder.isVisible()) { + await viewsFolder.click(); + await page.waitForTimeout(300); + await page.screenshot({ path: 'test-results/screenshots/sidebar-views-expanded.png' }); + } + }); +}); + +test.describe('Settings', () => { + test('should open Obsidian settings and find TaskNotes settings', async () => { + const { page } = app; + + // Open settings with Ctrl+, + await page.keyboard.press('Control+,'); + await page.waitForTimeout(500); + + const settingsModal = page.locator('.modal.mod-settings'); + await expect(settingsModal).toBeVisible({ timeout: 5000 }); + + await page.screenshot({ path: 'test-results/screenshots/obsidian-settings.png' }); + + // Look for TaskNotes in the plugin settings + const tasknotesSetting = page.locator('.vertical-tab-nav-item:has-text("TaskNotes")'); + if (await tasknotesSetting.isVisible()) { + await tasknotesSetting.click(); + await page.waitForTimeout(500); + await page.screenshot({ path: 'test-results/screenshots/tasknotes-settings.png' }); + } + + // Close settings + await page.keyboard.press('Escape'); + await page.waitForTimeout(300); + }); +}); + +// ============================================================================ +// DOCUMENTED UI ISSUES +// These test.fixme() tests document known UI issues discovered during exploration. +// When a fix is implemented, the test should be updated to pass. +// ============================================================================ + +test.describe('Documented UI Issues', () => { + test.fixme('bases views show "View Calendar not found" when opened before calendar view', async () => { + // Issue: When opening Kanban or Agenda views via command before any Calendar view + // has been opened in the session, the view shows "View 'Calendar' not found" error. + // + // Steps to reproduce: + // 1. Fresh Obsidian start with TaskNotes plugin + // 2. Open command palette (Ctrl+P) + // 3. Run "TaskNotes: Open kanban board" command + // 4. Observe error: "View 'Calendar' not found" + // + // Expected: Kanban view should render correctly without requiring Calendar first + // Actual: Error message displayed, view fails to load + // + // Workaround: Open any calendar-based view first (Calendar, Agenda, etc.) + // + // This appears to be a dependency issue where Bases views that use FullCalendar + // require FullCalendar to be initialized first via the Calendar view. + // + // See screenshots: test-results/screenshots/kanban-view-error.png + }); + + test.fixme('agenda view empty state could be more informative', async () => { + // Issue: The agenda view shows "No events to display" which doesn't clarify + // that it's looking for tasks, not calendar events. + // + // Suggestion: Change text to "No tasks scheduled in this time range" or similar + // to better match TaskNotes terminology. + }); + + test.fixme('task modal icon buttons lack visible labels or tooltips on hover', async () => { + // Issue: The task creation modal has a row of icon buttons (status, date, project, + // priority, recurrence, reminder) but they lack text labels or tooltips. + // + // Impact: New users may not understand what each icon does without trial and error. + // + // Suggestion: Add aria-label for accessibility and show tooltip on hover. + }); + + // FIXED: kanban-default Base view now correctly shows Kanban Board + // The issue was that the .base file had `type: tasknotesCalendar` instead of `type: tasknotesKanban` + // Fixed by updating the view type property in the e2e vault's .base files. + // Related: GitHub Issue #1397 + + test.fixme('tasks-default Base view shows no tasks despite tasks existing', async () => { + // Note: This is expected behavior - the tasks in the sidebar are task NOTES (markdown files + // with the #task tag), while the view filter requires `file.hasTag("task")`. The tasks + // visible in the sidebar are correctly created TaskNotes files. + // + // RELATED: GitHub Issue #1397 - May be related to view type mismatches causing + // incorrect view rendering or filter application. + // + // Steps to reproduce: + // 1. Have tasks created (visible in sidebar) + // 2. Click on tasks-default in the TaskNotes Views folder + // 3. Observe: "No TaskNotes tasks found for this Base" message + // + // Expected: Tasks should be listed in the view + // Actual: Empty state message shown + // + // This may be a filter/query issue with the default Base configuration, + // or the .base files may need regeneration after fix #1397. + // + // See screenshots: test-results/screenshots/tasks-view.png + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..6a625699 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: false, // Run tests sequentially - Obsidian is a single instance + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, // Single worker - Obsidian can only run one instance + reporter: 'list', + timeout: 60000, // 60 seconds per test + use: { + trace: 'retain-on-failure', + video: 'retain-on-failure', + screenshot: 'on', // Capture screenshots for all tests + }, + projects: [ + { + name: 'obsidian', + use: { ...devices['Desktop Chrome'] }, + }, + ], +});