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
This commit is contained in:
callumalpass 2025-12-30 21:22:51 +11:00
parent 0b104f0cb1
commit b4a1c63ed8
5 changed files with 790 additions and 0 deletions

26
e2e-launch.sh Executable file
View file

@ -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"

101
e2e-setup.sh Executable file
View file

@ -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."

140
e2e/obsidian.ts Normal file
View file

@ -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<ObsidianApp> {
// 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<void>((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<void>((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<void> {
if (app.browser) {
await app.browser.close();
}
if (app.process) {
app.process.kill();
}
}
export async function openCommandPalette(page: Page): Promise<void> {
// 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<void> {
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<void> {
const notice = page.locator('.notice');
await notice.waitFor({ timeout: 10000 });
if (text) {
await notice.filter({ hasText: text }).waitFor({ timeout: 5000 });
}
}

501
e2e/tasknotes.spec.ts Normal file
View file

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

22
playwright.config.ts Normal file
View file

@ -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'] },
},
],
});