mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-03-09 08:20:32 -07:00
* Create ErrorDuringRelease.md
* Enforce test timeouts and add root tiddler scenario
Set global and step timeouts to 5s (local) and 10s (CI) across cucumber config and step definitions to standardize test execution times. Add a new scenario to verify root tiddler configuration and content loading after restart. Enhance start-e2e-app script to accept or auto-detect test scenario names and pass them to the app.
* Improve error handling for window and view initialization
Enhanced error reporting and handling when browser windows are not ready or fail to register in windowService. Updated focus logic to dynamically retrieve the current browser window, improving reliability during workspace hibernation and wake-up scenarios.
* Remove AfterAll hook and add --exit to e2e tests
Eliminates the AfterAll hook that forced process exit in cleanup.ts to prevent hanging after tests. Adds the --exit flag to the cucumber-js command in the e2e test script to ensure proper test process termination.
* Add step to restart workspace in wiki tests
Introduces a new step definition 'I restart workspace {string}' to programmatically restart a wiki workspace during tests. Updates the root tiddler scenario to use this step for verifying lazy-load behavior after workspace restart, improving test reliability and clarity.
* Centralize and standardize E2E test timeouts
Extracted timeout values into features/supports/timeouts.ts and replaced hardcoded timeouts in step definitions with named constants. This ensures consistent timeout handling across local and CI environments, reduces duplication, and clarifies intent. Also improved workspace update logic to check watch-fs state before restart and cleaned up related log marker handling.
* Improve i18n coverage and add Windows installer log access
Expanded and unified i18n keys for error messages and UI labels across multiple languages. Refactored code to remove hardcoded default values from translation calls. Added a Developer Tools option to open the Windows installer log folder (SquirrelTemp) when running on Windows. Introduced a placeholder file to preserve dynamic i18n keys for error messages.
* Initialize Tidgi mini window before workspace views
Moved the initialization of the Tidgi mini window to occur before initializing all workspace views in main.ts to ensure correct view creation. Added a clarifying comment in DeveloperTools.tsx regarding the SquirrelSetup.log path.
* Refactor Tidgi mini window initialization logic
Tidgi mini window creation now only creates the window; view creation is deferred to initializeAllWorkspaceView. Updated related comments and logging for clarity. Also fixed formatting in French translations and improved documentation for error handling during release.
* Add model feature chips to model selection UI
Introduces a ModelFeatureChip component to visually display model features in the model selector and new model dialog. Updates defaultProviders to include new models with features, and enhances the UI to show feature chips for each model, improving clarity for users selecting models.
* Add image attachment support to chat messages
This update enables users to attach image files to chat messages, including UI changes for file selection and preview, backend persistence of attachments, and prompt concatenation logic to include images in AI requests. It also adds error handling and i18n for model vision support, updates message rendering to display images, and improves logging and API validation for vision-capable models.
* Improve streaming status handling for agent messages
Adds a failsafe to clear streaming status when an agent reaches a terminal state and refines logic to prevent marking completed messages as streaming. Also updates message stream completion in AgentInstanceService to ensure proper cleanup and delivery of IPC messages. Includes new feature tests for message streaming status and image upload scenarios.
* Add cross-window sync feature and test steps
Introduces a new feature file for cross-window synchronization scenarios. Adds step definitions to open workspaces in new windows and execute TiddlyWiki code programmatically. Removes obsolete wiki.ts.backup file and updates agentActions for related actions.
* feat(sync): fix cross-window synchronization via SSE
- Remove overly aggressive echo prevention in backend that blocked all SSE updates
- Backend now forwards all wiki change events to subscribers
- Add comprehensive cross-window sync tests verifying bidirectional updates
- Test main->new window sync and new->main window sync scenarios
- Version bump to 0.13.0-prerelease19
* Improve file attachment handling in chat and tests
Refactors file input handling in chat tests to use Playwright's setInputFiles, updates message sending types to support optional file attachments, and enhances file metadata persistence and logging. Adjusts test expectations and UI logic to better handle and display image attachments, and clarifies combobox value assertions in ExternalAPI tests.
* Add file input validation and improve i18n messages
Added image type and size validation (10MB limit) to file input in InputContainer. Improved image preview logic. Updated French, Japanese, and Russian translations with new error messages for missing/default model and vision support. Enhanced type safety in promptConcatWithImage tests and messagePersistence logging. Fixed race condition in ExternalAPIService lazy initialization. Updated CommitDetailsPanel to use common cancel translation key.
* review
* Update browserView.ts
* Update timeouts.ts
* Update cucumber.config.js
* Update cucumber.config.js
* Move global timeout config to separate module
Extracted global timeout setup from cucumber.config.js to features/supports/timeout-config.ts using setDefaultTimeout. This ensures the timeout is set via code rather than config, improving clarity and maintainability.
* Update newAgent.feature
427 lines
14 KiB
TypeScript
427 lines
14 KiB
TypeScript
import { DataTable, Then, When } from '@cucumber/cucumber';
|
|
import { backOff } from 'exponential-backoff';
|
|
import { parseDataTableRows } from '../supports/dataTable';
|
|
import {
|
|
clickElement,
|
|
clickElementWithText,
|
|
elementExists,
|
|
executeTiddlyWikiCode,
|
|
getDOMContent,
|
|
getTextContent,
|
|
isLoaded,
|
|
pressKey,
|
|
typeText,
|
|
} from '../supports/webContentsViewHelper';
|
|
import type { ApplicationWorld } from './application';
|
|
|
|
// Backoff configuration for retries
|
|
const BACKOFF_OPTIONS = {
|
|
numOfAttempts: 10,
|
|
startingDelay: 100,
|
|
timeMultiple: 2,
|
|
};
|
|
|
|
Then('I should see {string} in the browser view content', async function(this: ApplicationWorld, expectedText: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
if (!this.currentWindow) {
|
|
throw new Error('No current window available');
|
|
}
|
|
|
|
await backOff(
|
|
async () => {
|
|
const content = await getTextContent(this.app!);
|
|
if (!content || !content.includes(expectedText)) {
|
|
throw new Error(`Expected text "${expectedText}" not found`);
|
|
}
|
|
},
|
|
BACKOFF_OPTIONS,
|
|
).catch(async () => {
|
|
const finalContent = await getTextContent(this.app!);
|
|
throw new Error(
|
|
`Expected text "${expectedText}" not found in browser view content. Actual content: ${finalContent ? finalContent.substring(0, 200) + '...' : 'null'}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
Then('I should see {string} in the browser view DOM', async function(this: ApplicationWorld, expectedText: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
if (!this.currentWindow) {
|
|
throw new Error('No current window available');
|
|
}
|
|
|
|
await backOff(
|
|
async () => {
|
|
const domContent = await getDOMContent(this.app!);
|
|
if (!domContent || !domContent.includes(expectedText)) {
|
|
throw new Error(`Expected text "${expectedText}" not found in DOM`);
|
|
}
|
|
},
|
|
BACKOFF_OPTIONS,
|
|
).catch(async () => {
|
|
const finalDomContent = await getDOMContent(this.app!);
|
|
throw new Error(
|
|
`Expected text "${expectedText}" not found in browser view DOM. Actual DOM: ${finalDomContent ? finalDomContent.substring(0, 200) + '...' : 'null'}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
Then('the browser view should be loaded and visible', async function(this: ApplicationWorld) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
if (!this.currentWindow) {
|
|
throw new Error('No current window available');
|
|
}
|
|
|
|
await backOff(
|
|
async () => {
|
|
const isLoadedResult = await isLoaded(this.app!);
|
|
if (!isLoadedResult) {
|
|
throw new Error('Browser view not loaded');
|
|
}
|
|
},
|
|
{ ...BACKOFF_OPTIONS, numOfAttempts: 30 },
|
|
).catch(() => {
|
|
throw new Error('Browser view is not loaded or visible after multiple attempts');
|
|
});
|
|
});
|
|
|
|
When('I click on {string} element in browser view with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
try {
|
|
// Check if selector contains :has-text() pseudo-selector
|
|
const hasTextMatch = selector.match(/^(.+):has-text\(['"](.+)['"]\)$/);
|
|
|
|
if (hasTextMatch) {
|
|
// Extract base selector and text content
|
|
const baseSelector = hasTextMatch[1];
|
|
const textContent = hasTextMatch[2];
|
|
await clickElementWithText(this.app, baseSelector, textContent);
|
|
} else {
|
|
// Use regular selector
|
|
await clickElement(this.app, selector);
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`Failed to click ${elementComment} element with selector "${selector}" in browser view: ${error as Error}`);
|
|
}
|
|
});
|
|
|
|
When('I click on {string} elements in browser view with selectors:', async function(this: ApplicationWorld, _elementDescriptions: string, dataTable: DataTable) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
const rows = dataTable.raw();
|
|
const dataRows = parseDataTableRows(rows, 2);
|
|
const errors: string[] = [];
|
|
|
|
if (dataRows[0]?.length !== 2) {
|
|
throw new Error('Table must have exactly 2 columns: | element description | selector |');
|
|
}
|
|
|
|
for (const [elementComment, selector] of dataRows) {
|
|
try {
|
|
const hasTextMatch = selector.match(/^(.+):has-text\(['"](.+)['"]\)$/);
|
|
|
|
if (hasTextMatch) {
|
|
const baseSelector = hasTextMatch[1];
|
|
const textContent = hasTextMatch[2];
|
|
await clickElementWithText(this.app, baseSelector, textContent);
|
|
} else {
|
|
await clickElement(this.app, selector);
|
|
}
|
|
} catch (error) {
|
|
errors.push(`Failed to click ${elementComment} element with selector "${selector}" in browser view: ${error as Error}`);
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
throw new Error(`Failed to click elements in browser view:\n${errors.join('\n')}`);
|
|
}
|
|
});
|
|
|
|
Then('I wait for {string} element in browser view with selector {string}', async function(
|
|
this: ApplicationWorld,
|
|
elementComment: string,
|
|
selector: string,
|
|
) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
await backOff(
|
|
async () => {
|
|
const exists = await elementExists(this.app!, selector);
|
|
if (!exists) {
|
|
throw new Error(`Element "${elementComment}" with selector "${selector}" not found yet`);
|
|
}
|
|
},
|
|
{ ...BACKOFF_OPTIONS, numOfAttempts: 20, startingDelay: 200 },
|
|
).catch(() => {
|
|
throw new Error(`Element "${elementComment}" with selector "${selector}" did not appear in browser view after multiple attempts`);
|
|
});
|
|
});
|
|
|
|
When('I type {string} in {string} element in browser view with selector {string}', async function(this: ApplicationWorld, text: string, elementComment: string, selector: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
try {
|
|
await typeText(this.app, selector, text);
|
|
} catch (error) {
|
|
throw new Error(`Failed to type in ${elementComment} element with selector "${selector}" in browser view: ${error as Error}`);
|
|
}
|
|
});
|
|
|
|
When('I press {string} in browser view', async function(this: ApplicationWorld, key: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
try {
|
|
await pressKey(this.app, key);
|
|
} catch (error) {
|
|
throw new Error(`Failed to press key "${key}" in browser view: ${error as Error}`);
|
|
}
|
|
});
|
|
|
|
Then('I should not see {string} in the browser view content', async function(this: ApplicationWorld, unexpectedText: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
if (!this.currentWindow) {
|
|
throw new Error('No current window available');
|
|
}
|
|
|
|
// Wait a bit for UI to update
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// Check that text does not exist in content
|
|
const content = await getTextContent(this.app);
|
|
if (content && content.includes(unexpectedText)) {
|
|
throw new Error(`Unexpected text "${unexpectedText}" found in browser view content`);
|
|
}
|
|
});
|
|
|
|
Then('I should not see a(n) {string} element in browser view with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
if (!this.currentWindow) {
|
|
throw new Error('No current window available');
|
|
}
|
|
|
|
await backOff(
|
|
async () => {
|
|
const exists: boolean = await elementExists(this.app!, selector);
|
|
if (exists) {
|
|
throw new Error('Element still exists');
|
|
}
|
|
},
|
|
BACKOFF_OPTIONS,
|
|
).catch(() => {
|
|
throw new Error(`Element "${elementComment}" with selector "${selector}" was found in browser view after multiple attempts, but should not be visible`);
|
|
});
|
|
});
|
|
|
|
Then('I should see a(n) {string} element in browser view with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
if (!this.currentWindow) {
|
|
throw new Error('No current window available');
|
|
}
|
|
|
|
await backOff(
|
|
async () => {
|
|
const exists: boolean = await elementExists(this.app!, selector);
|
|
if (!exists) {
|
|
throw new Error('Element does not exist yet');
|
|
}
|
|
},
|
|
BACKOFF_OPTIONS,
|
|
).catch(() => {
|
|
throw new Error(`Element "${elementComment}" with selector "${selector}" not found in browser view after multiple attempts`);
|
|
});
|
|
});
|
|
|
|
Then('I should see {string} elements in browser view with selectors:', async function(this: ApplicationWorld, _elementDescriptions: string, dataTable: DataTable) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
if (!this.currentWindow) {
|
|
throw new Error('No current window available');
|
|
}
|
|
|
|
const rows = dataTable.raw();
|
|
const dataRows = parseDataTableRows(rows, 2);
|
|
const errors: string[] = [];
|
|
|
|
if (dataRows[0]?.length !== 2) {
|
|
throw new Error('Table must have exactly 2 columns: | element description | selector |');
|
|
}
|
|
|
|
await Promise.all(dataRows.map(async ([elementComment, selector]) => {
|
|
try {
|
|
await backOff(
|
|
async () => {
|
|
const exists: boolean = await elementExists(this.app!, selector);
|
|
if (!exists) {
|
|
throw new Error('Element does not exist yet');
|
|
}
|
|
},
|
|
BACKOFF_OPTIONS,
|
|
);
|
|
} catch (error) {
|
|
errors.push(`Element "${elementComment}" with selector "${selector}" not found in browser view: ${error as Error}`);
|
|
}
|
|
}));
|
|
|
|
if (errors.length > 0) {
|
|
throw new Error(`Failed to find elements in browser view:\n${errors.join('\n')}`);
|
|
}
|
|
});
|
|
|
|
When('I open tiddler {string} in browser view', async function(this: ApplicationWorld, tiddlerTitle: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
try {
|
|
// Use TiddlyWiki's addToStory API to open the tiddler
|
|
await executeTiddlyWikiCode(this.app, `$tw.wiki.addToStory("${tiddlerTitle.replace(/"/g, '\\"')}")`);
|
|
} catch (error) {
|
|
throw new Error(`Failed to open tiddler "${tiddlerTitle}" in browser view: ${error as Error}`);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Create a new tiddler with title and optional tags via TiddlyWiki UI.
|
|
* This step handles all the UI interactions: click add button, set title, add tags, and confirm.
|
|
*/
|
|
When('I create a tiddler {string} with tag {string} in browser view', async function(
|
|
this: ApplicationWorld,
|
|
tiddlerTitle: string,
|
|
tagName: string,
|
|
) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
// Click add tiddler button
|
|
await clickElement(this.app, 'button:has(.tc-image-new-button)');
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
// Click on title input
|
|
await clickElement(this.app, "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor");
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
// Select all and delete to clear the default title
|
|
await pressKey(this.app, 'Control+a');
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
await pressKey(this.app, 'Delete');
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
// Type the tiddler title
|
|
await typeText(this.app, "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor", tiddlerTitle);
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// Click on tag input
|
|
await clickElement(this.app, "div[data-tiddler-title^='Draft of'] div.tc-edit-add-tag-ui input.tc-edit-texteditor[placeholder='标签名称']");
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
// Type the tag name
|
|
await typeText(this.app, "div[data-tiddler-title^='Draft of'] div.tc-edit-add-tag-ui input.tc-edit-texteditor[placeholder='标签名称']", tagName);
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
// Click add tag button
|
|
await clickElement(this.app, "div[data-tiddler-title^='Draft of'] span.tc-add-tag-button button");
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
// Click confirm button to save
|
|
await clickElement(this.app, 'button:has(.tc-image-done-button)');
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
});
|
|
|
|
/**
|
|
* Create a new tiddler with title and custom field via TiddlyWiki UI.
|
|
*/
|
|
When('I create a tiddler {string} with field {string} set to {string} in browser view', async function(
|
|
this: ApplicationWorld,
|
|
tiddlerTitle: string,
|
|
fieldName: string,
|
|
fieldValue: string,
|
|
) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
// Click add tiddler button
|
|
await clickElement(this.app, 'button:has(.tc-image-new-button)');
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
// Click on title input
|
|
await clickElement(this.app, "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor");
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
// Select all and delete to clear the default title
|
|
await pressKey(this.app, 'Control+a');
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
await pressKey(this.app, 'Delete');
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
// Type the tiddler title
|
|
await typeText(this.app, "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor", tiddlerTitle);
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// Add the custom field
|
|
await clickElement(this.app, "div[data-tiddler-title^='Draft of'] .tc-edit-field-add-name-wrapper input");
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
await typeText(this.app, "div[data-tiddler-title^='Draft of'] .tc-edit-field-add-name-wrapper input", fieldName);
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
await clickElement(this.app, "div[data-tiddler-title^='Draft of'] .tc-edit-field-add-value input");
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
await typeText(this.app, "div[data-tiddler-title^='Draft of'] .tc-edit-field-add-value input", fieldValue);
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
await clickElement(this.app, "div[data-tiddler-title^='Draft of'] .tc-edit-field-add button");
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
// Click confirm button to save
|
|
await clickElement(this.app, 'button:has(.tc-image-done-button)');
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
});
|
|
|
|
/**
|
|
* Execute TiddlyWiki code in browser view
|
|
* Useful for programmatic wiki operations
|
|
*/
|
|
When('I execute TiddlyWiki code in browser view: {string}', async function(this: ApplicationWorld, code: string) {
|
|
if (!this.app) {
|
|
throw new Error('Application not launched');
|
|
}
|
|
|
|
try {
|
|
// Wrap the code to avoid returning non-serializable objects
|
|
const wrappedCode = `(function() { ${code}; return true; })()`;
|
|
await executeTiddlyWikiCode(this.app, wrappedCode);
|
|
} catch (error) {
|
|
throw new Error(`Failed to execute TiddlyWiki code in browser view: ${error as Error}`);
|
|
}
|
|
});
|