mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
- 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
501 lines
18 KiB
TypeScript
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
|
|
});
|
|
});
|