* fix: not removed
* Optimize tidgi.config.json writes for workspace updates
Update logic to write tidgi.config.json only for the modified workspace instead of all wiki workspaces on each update. This reduces redundant file operations and improves performance during workspace updates.
* Refactor workspace saving and UI update logic
Introduced a private saveWorkspacesToSettings method to centralize logic for saving workspaces and removing syncable fields from wiki workspaces. The set and setWorkspaces methods now support skipping UI updates for batch operations, improving performance. Fixed minor issues in legacy migration and error messages.
* Add 'Ask AI' context menu and wiki embed split view
Introduces an 'Ask AI' option to the wiki context menu, enabling users to send selected text to an agent chat in a split view with the wiki embedded. Implements new tab type WIKI_EMBED, updates tab and channel types, adds localization, manages BrowserView bounds for embedding, and ensures persistence and IPC wiring for the new workflow.
* Update wiki
* electron chrome mcp mode sometimes wont show browser view
Clarified troubleshooting steps in docs/MCP.md regarding browser view issues and updated the instructions. Reordered the 'start:dev:mcp' script in package.json for better organization.
* Add agent selection to 'Talk with AI' context menu
Replaces the 'Ask AI' context menu with 'Talk with AI' and adds a submenu for selecting different agent definitions. Updates translations for all supported languages, modifies the askAIWithSelection channel to support agentDefId, and refactors tab creation logic to support split view with agent selection. Improves robustness in view management by handling case-insensitive workspace IDs and custom bounds logic.
* Add e2e test and refactor 'Talk with AI' split view logic
Introduces a new Cucumber feature for 'Talk with AI' from wiki selection, adds a step definition to trigger the workflow via IPC, and refactors split view tab creation to reuse existing tabs when possible. Updates the agent browser service to support finding or creating the appropriate split view tab, and adjusts menu and view services for improved robustness and code clarity. Also adds test IDs to relevant components for more reliable UI testing.
* Update defaultWiki.feature
* Add config error handling and i18n for agent errors
Introduces a new feature test for configuration error handling, adds step definition to remove AI settings for testing, and updates error message rendering to support new error types. Internationalized error messages and button labels for configuration issues are added in both English and Chinese locales. The error message renderer now uses a data-testid for easier testing and recognizes additional error types as fixable in settings.
* Refactor feature files to use two-column selector tables
Updated all feature files to use a standardized two-column format for selector tables, with explicit 'element description' and 'selector' columns. Step definitions in ui.ts were refactored to support this format, improving readability and maintainability of test steps and error handling.
* Delete tiddlywiki
* test: allow parallel
* test: implement scenario isolation for E2E tests
- Isolate each test scenario in test-artifacts/{scenarioSlug}/ directory
- Use dynamic ports for mock OpenAI server to avoid port conflicts
- Log VIEW_LOADED event via did-finish-load in main process (more reliable)
- Search all .log files when waiting for log markers
- Increase timeout for log marker steps to 15 seconds
- Fix ts-node cache issues by clearing cache before tests
- Move application launch to individual scenarios (required for mock server setup)
All 45 E2E test scenarios now pass consistently.
* refactor: optimize agent.feature by moving common steps to Background
- Add MockOpenAIServer.addRules() method to append responses dynamically
- Add 'I have started the mock OpenAI server without rules' step for Background
- Add 'I add mock OpenAI responses:' step to inject responses per scenario
- Move application launch and navigation to Background (shared by all scenarios)
- Keep scenario-specific mock responses in individual scenarios
This improves test maintainability by reducing duplication while keeping
scenario-specific configuration flexible.
* lint
* Refactor scenario path helpers into shared module
Moved scenario-specific path helper functions from individual step definition files to a centralized 'features/supports/paths.ts' module. Updated imports in step definitions to use the shared helpers, improving code reuse and maintainability. Also enhanced test for ContextService to skip optional runtime keys.
* Refactor slug generation to use shared slugify helper
Introduced a new src/helpers/slugify.ts utility for consistent slug generation across the codebase. Updated appPaths.ts to use the shared slugify function, improving maintainability and ensuring identical behavior for test scenario slugs. Added documentation and clarified slugification rules in relevant files. Minor comments and clarifications were added to E2E and mock server code.
* Enforce strict timeout rules in E2E test steps
Added and clarified critical warnings for AI agents regarding timeout modifications in application, cleanup, and wiki step definitions. All timeouts are now strictly limited to 5s local and 10s CI, with explicit comments and environment-based values. Updated documentation and code comments to reinforce that timeouts indicate real bugs and should not be increased.
* Update features/stepDefinitions/application.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Refactor E2E helpers, improve test reliability and cleanup
Centralizes data table parsing for UI step definitions, refactors mock OpenAI server setup, and improves workspace settings path handling for tests. Adjusts timeouts for window and app closing to better reflect real-world performance. Fixes type usage in workspace ID lookups and adds error handling for resize observer and cleanup in WikiEmbedTabContent. Enhances agent browser tab logic and view service cleanup to prevent memory leaks and catch workspace ID casing issues.
* Update agent.ts
* fix: resolve all E2E test timeout issues
* fix: improve CI test reliability with better timing and cleanup
- Use exponential-backoff library for agent creation retry logic
- Extend agent cancel delay to 1000ms for CI environments
- Fix git log refresh marker timing with queueMicrotask
- Improve cleanup timeout handling with force close strategy
All E2E tests passing locally including previously failing CI tests.
* Improve Git log E2E signal and add debug logging
Renames the test artifact in the CI workflow for clarity. Moves the E2E test timing log in useGitLogData to after entries are rendered, using a more reliable signal. Adds a debug log to notifyGitStateChange for better traceability.
* test-artifacts-ci
* Optimize test artifact handling and Git log logging
Update CI workflow to clean up large cache folders in test artifacts and only upload logs, settings, and screenshots to reduce artifact size. Refactor useGitLogData to log immediately after state updates for improved E2E test reliability, removing unnecessary setTimeout.
* Update useGitLogData.ts
* Improve Git log E2E test logging and .gitignore
Added 'test-artifacts-ci.zip' to .gitignore. Moved the '[test-id-git-log-refreshed]' log to immediately after data load for more reliable E2E test detection, and removed redundant logging from the render effect in useGitLogData.ts.
* Update useGitLogData.ts
* Update useGitLogData.ts
* Fix git log refresh marker not appearing in CI
- Move git-log-refreshed marker before RAF to ensure it's recorded
- RAF callbacks may not execute reliably in headless CI environments
- Add debug logging to track loadGitLog execution
- Add try-catch around log call to catch any errors
- Keep git-log-data-rendered in useEffect for UI tracking
* Update useGitLogData.ts
* Update useGitLogData.ts
* Add comprehensive logging to diagnose git-log-refreshed issue
- Log before RAF and inside RAF to pinpoint exact failure location
- Add try-catch to capture any errors
- Two log markers: before-raf and in-raf
- This will definitively show where the logging fails in CI
* Fix race condition: prevent concurrent loadGitLog calls
Root cause: commit triggers 2 refreshes (gitStateChange$ + handleCommitSuccess)
- First loadGitLog (refreshTrigger=1) succeeds
- Second loadGitLog (refreshTrigger=2) starts but never completes
- Add loadGitLogInProgress guard to prevent concurrent execution
- Log when loadGitLog is skipped due to in-progress call
This ensures git-log-refreshed is always logged after commit.
* Remove redundant triggerRefresh calls causing race condition
- handleCommitSuccess/Revert/Undo no longer call triggerRefresh
- gitStateChange\$ observable already triggers refresh for these operations
- Redundant calls caused 2 concurrent loadGitLog, causing CI test failures
- Local tests passed because both completed; CI failed because 2nd never completed
This ensures only 1 loadGitLog runs per git operation.
* Remove unused triggerRefresh parameter from useCommitSelection
- triggerRefresh no longer used in handlers
- Remove from interface and call site
- Clean up lint errors
* Remove triggerRefresh completely - no longer needed
- Observable subscription handles all git state changes
- Remove function definition and exports
- Fix all lint errors
Root cause resolved: commit triggered double refresh causing race condition.
Now only single refresh via observable.
* Remove fixed time waits from gitLog.feature and fix race condition
- Remove all fixed time wait steps from gitLog.feature (14 instances)
- Remove redundant triggerRefresh calls in handleCommitSuccess/Revert/Undo
- Add loadGitLogInProgress guard to prevent concurrent loadGitLog
- Root cause: commit triggered 2 refreshes causing race condition
- Only gitStateChange\$ observable now triggers refresh
- All 4 gitLog tests pass locally
* Fix clear timing: clear log BEFORE commit, not after
Root cause: test cleared git-log-refreshed AFTER commit completed
- But commit already triggered refresh and logged git-log-refreshed
- Clear deleted it, then test waited for new log that would never come
- Solution: clear BEFORE clicking commit button
- This way commit's git-log-refreshed is the first one after clear
Test now passes locally.
* Update cleanup.ts
* Initial commit when init a new git.
* Refactor feature steps for multi-element and log marker tables
Updated multiple feature files and step definitions to support table-driven steps for clicking and asserting multiple elements, and for waiting for multiple log markers in sequence. This reduces redundant waits, improves test reliability, and streamlines Gherkin syntax for multi-element actions and assertions. Also removed unnecessary manual wait steps where content or element checks now handle waiting automatically.
* Minor code cleanup and formatting improvements
Reordered imports in browserView.ts, fixed whitespace in cleanup.ts and useGitLogData.ts, and improved line formatting in GitLog/index.tsx for better readability and consistency.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: tidgi <tiddlygit@gmail.com>
17 KiB
Testing Guide
Testing guide for TidGi-Desktop using Vitest + React Testing Library for unit tests and Playwright + Cucumber for E2E tests.
Quick Start
# Run all tests
pnpm test
# Run unit tests only
pnpm test:unit
# Run E2E tests (requires prepare packaged app, but only when you modify code in ./src) Don't need to run this if you only modify .feature file or step definition ts files.
pnpm run test:prepare-e2e
# (When only modify tests in ./features folder, and you have packaged app before, only need to run this.)
pnpm test:e2e
# Or run a specific e2e test by using same `@xxx` as in the `.feature` file.
pnpm test:e2e --tags="@smoke"
# Or run a single e2e test by `--name`
pnpm test:e2e --name "Wiki-search tool usage" # Not `-- --name` , and not `name`, is is just `--name` and have "" around the value, not omitting `--name`
# Don't directly concat filename after pnpm test:e2e, only unit test can do that, e2e test can't.
# Run with coverage
pnpm test:unit -- --coverage
# Run a single test file to reduce execution time when fixing an issue.
pnpm test:unit src/services/agentDefinition/__tests__/responsePatternUtility.test.ts
# Start packed e2e electron app manually to see what's going on as a human (AI agent is not allowed to run this, can only run commands above)
cross-env NODE_ENV=test pnpm dlx tsx ./scripts/start-e2e-app.ts
Except for above parameters, AI agent can't use other parameters, otherwise complex shell command usage or parameters will require human approval and may not passed.
Long running script
prepare and test may run for a long time. Don't execute any shell command like echo "waiting" or Start-Sleep -Seconds 5;, they are useless, and only will they interrupt the command. You need to check active terminal output in a loop until you see it is truly done.
Project Setup
Test Configuration: TypeScript-first with vitest.config.ts
- Unit tests: Vitest + React Testing Library + jsdom
- E2E tests: Playwright + Cucumber
- Coverage: HTML reports in
coverage/
Related file structure:
src/
├── __tests__/ # Global test setup & utilities
├── components/*/
│ └── __tests__/ # Component tests
└── services/*/
└── __tests__/ # Service tests
features/ # E2E tests
├── *.feature # Gherkin scenarios
├── stepDefinitions/ # Playwright implementations
└── supports/ # Test utilities
out/ # `test:prepare-e2e` Bundled production app to test
userData-test/ # User setting folder created during `test:e2e`
userData-dev/ # User setting folder created during `start:dev`
wiki-test/ # containing wiki folders created during `test:e2e`
wiki-dev/ # containing wiki folders created during `start:dev`
Writing Unit Tests
Code here are truncated or shorten. You should always read actuarial test file to learn how to write.
Component Testing Best Practices
// Use semantic queries and user-event for realistic interactions
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('WorkspaceSelector', () => {
it('should switch to Help page when clicking Help workspace', async () => {
const user = userEvent.setup();
render(<WorkspaceSelector />);
// Wait for async content to load
expect(await screen.findByText('Guide Page Content')).toBeInTheDocument();
// Use realistic user interactions
const helpText = await screen.findByText('Help');
await user.click(helpText);
// Assert on user-visible changes
expect(await screen.findByText('Help Page Content')).toBeInTheDocument();
});
});
Effective Mocking
// Mock complex components simply
vi.mock('../ComplexComponent', () => ({
default: () => <div data-testid='complex-component'>Mocked Component</div>,
}));
// Test-specific data for current test file
const workspacesSubject = new BehaviorSubject([
{ id: 'test-workspace', name: 'Test Wiki' },
]);
// Override global observables for this test
Object.defineProperty(window.observables.workspace, 'workspaces$', {
value: workspacesSubject.asObservable(),
writable: true,
});
Global Mock Management
Centralize common mocks in src/__tests__/__mocks__/ directory, and import them in src/__tests__/setup-vitest.ts:
- Services from window APIs (
window.service,window.remote,window.observables) and container APIs (@services/container) are now mocked insrc/__tests__/__mocks__/window.ts和services-container.ts - Common libraries (
react-i18nextinreact-i18next.ts, logger inservices-log.ts)
Most of services should be in these mock files. Only mock specific small set of service API in new test files if needed.
Override in test files only when you need test-specific data:
// Only override what's specific to this test
Object.defineProperty(window.observables.workspace, 'workspaces$', {
value: testSpecificWorkspaces$.asObservable(),
writable: true,
});
This keeps tests focused and reduces duplication across test files.
Async Testing Patterns
// Use findBy* for elements that appear asynchronously
expect(await screen.findByText('Loading complete')).toBeInTheDocument();
// Use waitForElementToBeRemoved for disappearing elements
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
// Avoid unnecessary waitFor - prefer findBy*
// ❌ Don't do this
await waitFor(() => {
expect(screen.getByText('Content')).toBeInTheDocument();
});
// ✅ Do this instead
expect(await screen.findByText('Content')).toBeInTheDocument();
// Handle async component initialization to avoid act(...) warnings
// ✅ Create helper that waits for async loading
const renderComponent = async () => {
const result = render(<AsyncComponent />);
await waitFor(() => {
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
return result;
};
// ✅ Use in tests
it('should test feature after loading', async () => {
await renderComponent();
// Now safe to test without act warnings
});
// ✅ For loading state tests, wait after assertion
it('should show loading initially', async () => {
render(<AsyncComponent />);
expect(screen.getByText('Loading')).toBeInTheDocument();
// Wait for completion to prevent warnings in subsequent async updates
await waitFor(() => {
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
});
Writing E2E Tests
Feature File Example
# features/agent.feature
Feature: Agent Workflow
Background:
Given I launch the TidGi application
And I wait for the page to load completely
@agent
Scenario: Complete agent workflow
# Use generic steps for common UI interactions
When I click on a "settings button" element with selector "#open-preferences-button"
When I switch to "preferences" window
When I type "TestProvider" in "provider name input" element with selector "[data-testid='new-provider-name-input']"
# ... more generic steps
Then I should see 4 messages in chat history
Step Definitions Architecture
The E2E testing framework uses a World-based architecture with Playwright + Cucumber:
// features/stepDefinitions/application.ts - Generic application steps
export class ApplicationWorld {
app: ElectronApplication | undefined;
// ...
}
// Generic step definitions you usually must reuse.
When('I click on a(n) {string} element with selector {string}', async function(elementComment: string, selector: string) {
// ...
});
// Don't define specific step only for you own use, that would be selfish.
When('(Dont do this) I click on a specific button and wait for 2 seconds.', async function() {
// Strictly forbidden.
});
Key E2E Testing Patterns
- Window Management: Use
getWindow()with retry logic for reliable window switching - Generic Steps: Reusable steps for common UI interactions with descriptive selectors
- Domain Steps: Specific steps for complex workflows (like agent conversations)
- Mock Services: Use tagged cleanup for feature-specific resources
- Streaming Support: Special handling for real-time updates in chat interfaces
- Don't think about adding new step definitions or change timeout duration, unless human ask you to do. You should always reuse existing steps, and debug the fundamental reason that causes timeout. Timeout usually because of expected element not percent.
- If you forget to run
pnpm run test:prepare-e2eafter modify code in./srcfolder, you may find expected elements missing. - Usually don't need to add wait time, because most check already will wait for a while. Should use exact test-id to wait internal steps, and test-id should contribute larger than 2 second waiting, otherwise it is useless.
Testing Library Best Practices
Important Testing Rules:
- Do NOT simplify tests - write comprehensive, professional unit tests
- Can add test-ids when accessibility queries aren't practical
- Do NOT be lazy - fix ALL tests until
pnpm test:unitpasses completely - Do NOT summarize until ALL unit tests pass
- Focus on professional, fix all seemly complex unit tests before moving to E2E
Query Priority (use in this order)
- Accessible queries -
getByRole,getByLabelText,getByPlaceholderText - Semantic queries -
getByAltText,getByTitle - Test IDs -
getByTestId(when accessibility queries aren't practical)
Async Patterns
- Use
findBy*instead ofgetBy*+waitFor - Use
user-eventinstead offireEventfor realistic interactions - Wait for initial async state in
beforeEachto avoid act() warnings
Common Antipatterns to Avoid
// ❌ Testing implementation details
expect(component.state.isLoading).toBe(false);
// ✅ Testing user-visible behavior
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
// ❌ Using act() wrapper unnecessarily
act(() => {
fireEvent.click(button);
});
// ✅ Using proper async testing
const user = userEvent.setup();
await user.click(button);
// ❌ Not handling async component initialization
render(<AsyncComponent />);
expect(screen.getByText('Content')).toBeInTheDocument(); // May cause act warnings
// ✅ Wait for async initialization to complete
const renderAsync = async () => {
const result = render(<AsyncComponent />);
await waitFor(() => expect(screen.queryByText('Loading')).not.toBeInTheDocument());
return result;
};
For complete Testing Library guidance, see Testing Library docs.
Viewing e2e tests
We check isTest when xxxWindow.show(), so it won't popup while testing. You can clear the desktop windows so you can see it.
Log
When AI is fixing issues, you can let it add more logs for troubleshooting, and then show the latest test log files or dev log files to the AI. Of course, it's best to run tests using pnpm test:unit, as it's fast and can be automated by AI without manual intervention. The logs should also be visible in the test, just change the mock of logger to use console log, and run a single test to get minimal logs.
If you want to send frontend log to the log file, you can't directly use import { logger } from '@services/libs/log'; you need to use void window.service.native.log('error', 'Renderer: xxx', { ...additionalMetadata });.
Otherwise you will get Can't resolve 'os' error
Only use VSCode tool to read file. Don't ever use shell command to read file. Use shell command to read file may be immediately refused by user, because he don't want to manually approve shell commands.
User profile
When running tests — especially E2E or other tests that start an Electron instance — the test runner will set Electron's userData to userData-test. This ensures the test process uses a separate configuration and data directory from any development or production TidGi instance, and prevents accidental triggering of Electron's single-instance lock.
src/constants/appPaths.ts: in test mode we callapp.setPath('userData', path.resolve(sourcePath, '..', 'userData-test'))to redirect settings and cache.src/helpers/singleInstance.ts: the main process usesapp.requestSingleInstanceLock()to enforce single-instance behavior; without a separateuserDatadirectory, a running local TidGi could conflict with test instances and cause one of them to exit.
For this reason, test workflows in this project (for example when running pnpm test:e2e or CI integration tests) need to do with cross-env NODE_ENV=test so it creates isolate state in userData-test.
Errors
close timed out after 10000ms / FILEHANDLE (unknown stack trace)
This happens because Electron/Vitest child processes do not inherit the ELECTRON_RUN_AS_NODE environment variable, so resources cannot be cleaned up and handles leak.
Do not set ELECTRON_RUN_AS_NODE in vitest.config.ts via process.env.ELECTRON_RUN_AS_NODE = 'true' — this only affects the main process, not child processes.
Always use cross-env in your test script. For example:
cross-env ELECTRON_RUN_AS_NODE=1 pnpm exec ./node_modules/.bin/electron ./node_modules/vitest/vitest.mjs run
Or run manually in shell: $env:ELECTRON_RUN_AS_NODE=1; pnpm run test:unit
We use ELECTRON_RUN_AS_NODE to solve native modules (like better-sqlite3) being compiled for the wrong Node.js version, see the section in ErrorDuringStart.md.
Module did not self-register: '/home/runner/work/TidGi-Desktop/TidGi-Desktop/node_modules/better-sqlite3/build/Release/better_sqlite3.node'
May needs pnpm exec electron-rebuild -f -w better-sqlite3.
An update to Component inside a test was not wrapped in act(...)
This warning occurs when React components perform asynchronous state updates during test execution. Common causes:
- Components with
useEffectthat fetch data on mount - Async API calls that update component state
- Timers or intervals that trigger state changes
- RxJS Observable subscriptions that trigger state updates
Solution: Wait for async operations to complete using helper functions:
// Create async render helper
const renderAsyncComponent = async () => {
const result = render(<AsyncComponent />);
// Wait for loading to complete
await waitFor(() => {
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
return result;
};
// Use in tests
it('should test feature', async () => {
await renderAsyncComponent();
// Now safe to interact without warnings
});
// For tests that trigger state updates, wait for UI to stabilize
it('should update when data changes', async () => {
render(<Component />);
// Trigger update
someObservable.next(newData);
// Wait for UI to reflect the change
await waitFor(() => {
expect(screen.getByText('Updated Content')).toBeInTheDocument();
});
});
Avoid explicitly using act() - React Testing Library handles most cases automatically when using proper async patterns.
Critical: To avoid act warnings with RxJS Observables:
- Never call
.next()on BehaviorSubject during test execution - Set all data before rendering - Don't trigger Observable updates via mocked APIs - Test the component's configuration, not the full update cycle
- For loading state tests - Unmount immediately after assertion to prevent subsequent updates
- Follow the Main component test pattern - Create Observables at file scope, never update them in tests
Example of correct Observable testing:
// ❌ Wrong: Updating Observable during test
it('should update when data changes', async () => {
render(<Component />);
preferencesSubject.next({ setting: false }); // This causes act warnings!
await waitFor(...);
});
// ✅ Correct: Observable created at file scope, never updated
const preferencesSubject = new BehaviorSubject({ setting: true });
describe('Component', () => {
it('should render correctly', async () => {
const { unmount } = render(<Component />);
expect(screen.getByText('Content')).toBeInTheDocument();
// Optional: unmount() if testing transient state
});
});
E2E test open production app
See User profile section above, we need to set NODE_ENV as test to open with correct profile.
This is done by using EnvironmentPlugin in webpack.plugins.js. Note that EsbuildPlugin's define doesn't work, it won't set env properly.
E2E test hang, and refused to exit until ctrl+C
Check features/stepDefinitions/application.ts to see if After step includes all clean up steps, like closing all windows instances before closing the app, and stop all utility servers.
Global shortcut not working
See src/helpers/testKeyboardShortcuts.ts