import { Then, When } from '@cucumber/cucumber'; import { backOff } from 'exponential-backoff'; 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', { timeout: 15000 }, 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: 15 }, ).catch(() => { throw new Error('Browser view is not loaded or visible after multiple attempts'); }); }); Then('I wait for {string} element in browser view with selector {string}', { timeout: 15000 }, 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 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 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`); }); }); 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', { timeout: 20000 }, 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', { timeout: 20000 }, 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)); });