callumalpass_tasknotes/e2e/tasknotes.spec.ts
callumalpass b4a1c63ed8 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
2025-12-31 09:34:09 +11:00

501 lines
18 KiB
TypeScript

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