diff --git a/features/filesystemPlugin.feature b/features/filesystemPlugin.feature index 206f8e84..b55f9f97 100644 --- a/features/filesystemPlugin.feature +++ b/features/filesystemPlugin.feature @@ -12,83 +12,6 @@ Feature: Filesystem Plugin Then the browser view should be loaded and visible And I wait for SSE and watch-fs to be ready - @file-watching @subwiki - Scenario: Tiddler with tag saves to sub-wiki folder - # Create sub-workspace linked to the default wiki - When I click on an "add workspace button" element with selector "#add-workspace-button" - And I switch to "addWorkspace" window - # Toggle to sub-workspace mode by clicking the switch - And I click on a "main/sub workspace switch" element with selector "[data-testid='main-sub-workspace-switch']" - # Select the first (default) wiki workspace from dropdown - And I select "wiki" from MUI Select with test id "main-wiki-select" - # Type folder name - And I type "SubWiki" in "sub wiki folder name input" element with selector "input[aria-describedby*='-helper-text'][value='wiki']" - And I type "TestTag" in "tag name input" element with selector "[data-testid='tagname-autocomplete-input']" - And I click on a "create sub workspace button" element with selector "button.MuiButton-colorSecondary" - And I switch to "main" window - Then I should see a "SubWiki workspace" element with selector "div[data-testid^='workspace-']:has-text('SubWiki')" - # Wait for main wiki to restart after sub-wiki creation - Then I wait for "main wiki restarted after sub-wiki creation" log marker "[test-id-MAIN_WIKI_RESTARTED_AFTER_SUBWIKI]" - And I wait for "watch-fs stabilized after restart" log marker "[test-id-WATCH_FS_STABILIZED]" - And I wait for "SSE ready after restart" log marker "[test-id-SSE_READY]" - Then I wait for "view loaded" log marker "[test-id-VIEW_LOADED]" - # Click SubWiki workspace again to ensure TestTag tiddler is displayed - And I wait for 1 seconds - When I click on a "SubWiki workspace button" element with selector "div[data-testid^='workspace-']:has-text('SubWiki')" - And I wait for 1 seconds - # Verify TestTag tiddler is visible - And I should see "TestTag" in the browser view content - # Create tiddler with tag to test file system plugin - And I click on "add tiddler button" element in browser view with selector "button:has(.tc-image-new-button)" - # Focus on title input, clear it, and type new title in the draft tiddler - And I click on "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor" - And I wait for 0.2 seconds - And I press "Control+a" in browser view - And I wait for 0.2 seconds - And I press "Delete" in browser view - And I type "TestTiddlerTitle" in "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor" - # Wait for tiddler state to settle, otherwise it still shows 3 chars (新条目) for a while - And I wait for 2 seconds - Then I should see "16 chars" in the browser view content - # Input tag by typing in the tag input field - use precise selector to target the tag input specifically - And I click on "tag input" element in browser view with selector "div[data-tiddler-title^='Draft of'] div.tc-edit-add-tag-ui input.tc-edit-texteditor[placeholder='标签名称']" - And I wait for 0.2 seconds - And I press "Control+a" in browser view - And I wait for 0.2 seconds - And I press "Delete" in browser view - And I wait for 0.2 seconds - And I type "TestTag" in "tag input" element in browser view with selector "div[data-tiddler-title^='Draft of'] div.tc-edit-add-tag-ui input.tc-edit-texteditor[placeholder='标签名称']" - # Click the add tag button to confirm the tag (not just typing) - And I wait for 0.2 seconds - And I click on "add tag button" element in browser view with selector "div[data-tiddler-title^='Draft of'] span.tc-add-tag-button button" - # Wait for file system plugin to save the draft tiddler to SubWiki folder, Even 3 second will randomly failed in next step. - And I wait for 4.5 seconds - # Verify the DRAFT tiddler has been routed to sub-wiki immediately after adding the tag - Then file "Draft of '新条目'.tid" should exist in "{tmpDir}/SubWiki" - # Verify the draft file is NOT in main wiki tiddlers folder (it should have been moved to SubWiki) - Then file "Draft of '新条目'.tid" should not exist in "{tmpDir}/wiki/tiddlers" - # Click confirm button to save the tiddler - And I click on "confirm button" element in browser view with selector "button:has(.tc-image-done-button)" - And I wait for 1 seconds - # Verify the final tiddler file exists in sub-wiki folder after save - # After confirming the draft, it should be saved as TestTiddlerTitle.tid in SubWiki - Then file "TestTiddlerTitle.tid" should exist in "{tmpDir}/SubWiki" - # Test SSE is still working after SubWiki creation - modify a main wiki tiddler - When I modify file "{tmpDir}/wiki/tiddlers/Index.tid" to contain "Main wiki content modified after SubWiki creation" - Then I wait for tiddler "Index" to be updated by watch-fs - # Confirm Index always open - Then I should see a "Index tiddler" element in browser view with selector "div[data-tiddler-title='Index']" - Then I should see "Main wiki content modified after SubWiki creation" in the browser view content - # Test modification in sub-workspace via symlink - # Modify the tiddler file externally - need to preserve .tid format with metadata - When I modify file "{tmpDir}/SubWiki/TestTiddlerTitle.tid" to contain "Content modified in SubWiki symlink" - # Wait for watch-fs to detect the change - And I wait for 1 seconds for "watch-fs to detect file change" - Then I wait for tiddler "TestTiddlerTitle" to be updated by watch-fs - And I wait for 2 seconds - # Verify the modified content appears in the wiki - Then I should see "Content modified in SubWiki symlink" in the browser view content - @file-watching Scenario: External file creation syncs to wiki # Create a test tiddler file directly on filesystem diff --git a/features/stepDefinitions/browserView.ts b/features/stepDefinitions/browserView.ts index d831bda2..15a6181c 100644 --- a/features/stepDefinitions/browserView.ts +++ b/features/stepDefinitions/browserView.ts @@ -1,6 +1,6 @@ import { Then, When } from '@cucumber/cucumber'; import { backOff } from 'exponential-backoff'; -import { clickElement, clickElementWithText, elementExists, getDOMContent, getTextContent, isLoaded, pressKey, typeText } from '../supports/webContentsViewHelper'; +import { clickElement, clickElementWithText, elementExists, executeTiddlyWikiCode, getDOMContent, getTextContent, isLoaded, pressKey, typeText } from '../supports/webContentsViewHelper'; import type { ApplicationWorld } from './application'; // Backoff configuration for retries @@ -82,6 +82,28 @@ Then('the browser view should be loaded and visible', { timeout: 15000 }, async }); }); +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'); @@ -191,3 +213,16 @@ Then('I should see a(n) {string} element in browser view with selector {string}' 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}`); + } +}); diff --git a/features/stepDefinitions/wiki.ts b/features/stepDefinitions/wiki.ts index c85074d8..79d18e9a 100644 --- a/features/stepDefinitions/wiki.ts +++ b/features/stepDefinitions/wiki.ts @@ -1,10 +1,10 @@ -import { Then, When } from '@cucumber/cucumber'; +import { DataTable, Given, Then, When } from '@cucumber/cucumber'; import { exec as gitExec } from 'dugite'; import { backOff } from 'exponential-backoff'; import fs from 'fs-extra'; import path from 'path'; import type { IWorkspace } from '../../src/services/workspaces/interface'; -import { settingsPath, wikiTestRootPath, wikiTestWikiPath } from '../supports/paths'; +import { settingsDirectory, settingsPath, wikiTestRootPath, wikiTestWikiPath } from '../supports/paths'; import type { ApplicationWorld } from './application'; // Backoff configuration for retries @@ -54,8 +54,24 @@ export async function waitForLogMarker(searchString: string, errorMessage: strin } When('I cleanup test wiki so it could create a new one on start', async function() { + // Clean up main wiki folder if (fs.existsSync(wikiTestWikiPath)) fs.removeSync(wikiTestWikiPath); + // Clean up all sub-wiki folders in wiki-test directory (SubWiki*, SubWikiPreload, SubWikiTagTree, SubWikiFilter, etc.) + if (fs.existsSync(wikiTestRootPath)) { + const entries = fs.readdirSync(wikiTestRootPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name !== 'wiki') { + const subWikiPath = path.join(wikiTestRootPath, entry.name); + try { + fs.removeSync(subWikiPath); + } catch (error) { + console.warn(`Failed to remove sub-wiki folder ${entry.name}:`, error); + } + } + } + } + /** * Clean up log files to prevent reading stale logs from previous scenarios. * This is critical for tests that wait for log markers like [test-id-WATCH_FS_STABILIZED] or [test-id-git-commit-complete], @@ -108,9 +124,11 @@ When('I cleanup test wiki so it could create a new one on start', async function const filtered: Record = {}; for (const id of Object.keys(workspaces)) { const ws = workspaces[id]; - const name = ws.name; - if (name === 'wiki' || id === 'wiki') continue; - filtered[id] = ws; + // Keep only page-type workspaces (agent, help, guide, add), remove all wiki workspaces + // This includes main wiki and all sub-wikis + if ('pageType' in ws && ws.pageType) { + filtered[id] = ws; + } } // Write with exponential backoff retry logic to handle file locks @@ -264,6 +282,97 @@ Then('file {string} should not exist in {string}', { timeout: 15000 }, async fun } }); +/** + * Verify that a workspace in settings.json has a specific property set to a specific value + */ +Then('settings.json should have workspace {string} with {string} set to {string}', { timeout: 10000 }, async function( + this: ApplicationWorld, + workspaceName: string, + propertyName: string, + expectedValue: string, +) { + await backOff( + async () => { + if (!await fs.pathExists(settingsPath)) { + throw new Error(`settings.json not found at ${settingsPath}`); + } + + type SettingsFile = { workspaces?: Record } & Record; + const settings = await fs.readJson(settingsPath) as SettingsFile; + + if (!settings.workspaces) { + throw new Error('No workspaces found in settings.json'); + } + + // Find the workspace by name + const workspace = Object.values(settings.workspaces).find(ws => ws.name === workspaceName); + if (!workspace) { + const existingNames = Object.values(settings.workspaces).map(ws => ws.name).join(', '); + throw new Error(`Workspace "${workspaceName}" not found in settings.json. Existing workspaces: ${existingNames}`); + } + + // Get the property value + const actualValue = (workspace as unknown as Record)[propertyName]; + + // Convert expected value to appropriate type for comparison + let parsedExpectedValue: unknown = expectedValue; + if (expectedValue === 'true') parsedExpectedValue = true; + else if (expectedValue === 'false') parsedExpectedValue = false; + else if (expectedValue === 'null') parsedExpectedValue = null; + else if (!isNaN(Number(expectedValue))) parsedExpectedValue = Number(expectedValue); + + if (actualValue !== parsedExpectedValue) { + throw new Error(`Expected "${propertyName}" to be "${expectedValue}" but got "${String(actualValue)}"`); + } + }, + BACKOFF_OPTIONS, + ); +}); + +/** + * Verify that a workspace in settings.json has a property array that contains a specific value + */ +Then('settings.json should have workspace {string} with {string} containing {string}', { timeout: 10000 }, async function( + this: ApplicationWorld, + workspaceName: string, + propertyName: string, + expectedValue: string, +) { + await backOff( + async () => { + if (!await fs.pathExists(settingsPath)) { + throw new Error(`settings.json not found at ${settingsPath}`); + } + + type SettingsFile = { workspaces?: Record } & Record; + const settings = await fs.readJson(settingsPath) as SettingsFile; + + if (!settings.workspaces) { + throw new Error('No workspaces found in settings.json'); + } + + // Find the workspace by name + const workspace = Object.values(settings.workspaces).find(ws => ws.name === workspaceName); + if (!workspace) { + const existingNames = Object.values(settings.workspaces).map(ws => ws.name).join(', '); + throw new Error(`Workspace "${workspaceName}" not found in settings.json. Existing workspaces: ${existingNames}`); + } + + // Get the property value + const actualValue = (workspace as unknown as Record)[propertyName]; + + if (!Array.isArray(actualValue)) { + throw new Error(`Expected "${propertyName}" to be an array but got "${typeof actualValue}"`); + } + + if (!actualValue.includes(expectedValue)) { + throw new Error(`Expected "${propertyName}" to contain "${expectedValue}" but got [${actualValue.join(', ')}]`); + } + }, + BACKOFF_OPTIONS, + ); +}); + /** * Cleanup function for sub-wiki routing test * Removes test workspaces created during the test @@ -623,6 +732,195 @@ async function clearHibernationTestData() { } } +/** + * Setup a sub-wiki with optional settings and multiple pre-existing tiddlers. + * This creates the sub-wiki folder, tiddler files, and settings configuration + * so the app loads everything on first startup. + * + * @param subWikiName - Name of the sub-wiki folder + * @param tagName - Tag name for the sub-wiki routing + * @param options - Optional settings: includeTagTree, fileSystemPathFilter + * @param tiddlers - Array of {title, tags, content} objects from DataTable.hashes() + */ +async function setupSubWiki( + subWikiName: string, + tagName: string, + options: { + includeTagTree?: boolean; + fileSystemPathFilter?: string; + }, + tiddlers: Record[], +) { + // 1. Create sub-wiki folder + const subWikiPath = path.join(wikiTestRootPath, subWikiName); + await fs.ensureDir(subWikiPath); + + // 2. Create tiddler files + const now = new Date(); + const timestamp = now.toISOString().replace(/[-:T.Z]/g, '').slice(0, 17); + + for (const tiddler of tiddlers) { + const tiddlerFilePath = path.join(subWikiPath, `${tiddler.title}.tid`); + const tiddlerFileContent = `created: ${timestamp} +modified: ${timestamp} +tags: ${tiddler.tags} +title: ${tiddler.title} + +${tiddler.content} +`; + await fs.writeFile(tiddlerFilePath, tiddlerFileContent, 'utf-8'); + } + + // 3. Create main wiki folder structure (if not exists) + const mainWikiPath = wikiTestWikiPath; + const templatePath = path.join(process.cwd(), 'template', 'wiki'); + if (!await fs.pathExists(mainWikiPath)) { + await fs.copy(templatePath, mainWikiPath); + // Remove .git from template + await fs.remove(path.join(mainWikiPath, '.git')).catch(() => { /* ignore */ }); + } + + // 4. Update settings.json with both main wiki and sub-wiki workspaces + await fs.ensureDir(settingsDirectory); + let settings: { workspaces?: Record } & Record = {}; + if (await fs.pathExists(settingsPath)) { + settings = await fs.readJson(settingsPath); + } + + // Generate unique IDs + const mainWikiId = 'main-wiki-test-id'; + const subWikiId = `sub-wiki-${subWikiName}-test-id`; + + // Create main wiki workspace if not exists + if (!settings.workspaces) { + settings.workspaces = {}; + } + + // Check if main wiki already exists + const existingMainWiki = Object.values(settings.workspaces).find( + ws => 'wikiFolderLocation' in ws && ws.wikiFolderLocation === mainWikiPath, + ); + + const mainWikiIdToUse = existingMainWiki?.id ?? mainWikiId; + + if (!existingMainWiki) { + settings.workspaces[mainWikiId] = { + id: mainWikiId, + name: 'wiki', + wikiFolderLocation: mainWikiPath, + isSubWiki: false, + storageService: 'local', + backupOnInterval: true, + excludedPlugins: [], + enableHTTPAPI: false, + includeTagTree: false, + fileSystemPathFilterEnable: false, + fileSystemPathFilter: null, + tagNames: [], + userName: '', + order: 0, + port: 5212, + readOnlyMode: false, + tokenAuth: false, + tagName: null, + mainWikiToLink: null, + mainWikiID: null, + enableFileSystemWatch: true, + lastNodeJSArgv: [], + homeUrl: `tidgi://${mainWikiId}`, + gitUrl: null, + active: true, + hibernated: false, + hibernateWhenUnused: false, + lastUrl: null, + picturePath: null, + subWikiFolderName: 'subwiki', + syncOnInterval: false, + syncOnStartup: true, + transparentBackground: false, + } as unknown as IWorkspace; + } + + // Create sub-wiki workspace with optional settings + settings.workspaces[subWikiId] = { + id: subWikiId, + name: subWikiName, + wikiFolderLocation: subWikiPath, + isSubWiki: true, + mainWikiToLink: mainWikiPath, + mainWikiID: mainWikiIdToUse, + storageService: 'local', + backupOnInterval: true, + excludedPlugins: [], + enableHTTPAPI: false, + includeTagTree: options.includeTagTree ?? false, + fileSystemPathFilterEnable: Boolean(options.fileSystemPathFilter), + fileSystemPathFilter: options.fileSystemPathFilter ?? null, + tagNames: [tagName], + tagName: tagName, + userName: '', + order: 1, + port: 5213, + readOnlyMode: false, + tokenAuth: false, + enableFileSystemWatch: true, + lastNodeJSArgv: [], + homeUrl: `tidgi://${subWikiId}`, + gitUrl: null, + active: false, + hibernated: false, + hibernateWhenUnused: false, + lastUrl: null, + picturePath: null, + subWikiFolderName: 'subwiki', + syncOnInterval: false, + syncOnStartup: true, + transparentBackground: false, + } as unknown as IWorkspace; + + await fs.writeJson(settingsPath, settings, { spaces: 2 }); +} + +/** + * Setup a sub-wiki with tiddlers (basic, no special options) + */ +Given('I setup a sub-wiki {string} with tag {string} and tiddlers:', async function( + this: ApplicationWorld, + subWikiName: string, + tagName: string, + dataTable: DataTable, +) { + const rows = dataTable.hashes(); + await setupSubWiki(subWikiName, tagName, {}, rows); +}); + +/** + * Setup a sub-wiki with includeTagTree enabled and tiddlers + */ +Given('I setup a sub-wiki {string} with tag {string} and includeTagTree enabled and tiddlers:', async function( + this: ApplicationWorld, + subWikiName: string, + tagName: string, + dataTable: DataTable, +) { + const rows = dataTable.hashes(); + await setupSubWiki(subWikiName, tagName, { includeTagTree: true }, rows); +}); + +/** + * Setup a sub-wiki with custom filter and tiddlers + */ +Given('I setup a sub-wiki {string} with tag {string} and filter {string} and tiddlers:', async function( + this: ApplicationWorld, + subWikiName: string, + tagName: string, + filter: string, + dataTable: DataTable, +) { + const rows = dataTable.hashes(); + await setupSubWiki(subWikiName, tagName, { fileSystemPathFilter: filter }, rows); +}); + export { clearGitTestData, clearHibernationTestData, clearSubWikiRoutingTestData, clearTestIdLogs }; /** diff --git a/features/subWiki.feature b/features/subWiki.feature new file mode 100644 index 00000000..4e800d94 --- /dev/null +++ b/features/subWiki.feature @@ -0,0 +1,263 @@ +Feature: Sub-Wiki Functionality + As a user + I want sub-wikis to properly load tiddlers on startup + And I want to use tag tree filtering to organize tiddlers into sub-wikis + So that my content is automatically organized + + + @file-watching @subwiki + Scenario: Tiddler with tag saves to sub-wiki folder + # Setup: Create sub-wiki with tag BEFORE launching the app (fast setup) + Given I cleanup test wiki so it could create a new one on start + And I setup a sub-wiki "SubWiki" with tag "TestTag" and tiddlers: + | title | tags | content | + | TestTag | TestTag | Tag tiddler stub | + When I launch the TidGi application + And I wait for the page to load completely + Then I should see "page body and workspaces" elements with selectors: + | div[data-testid^='workspace-']:has-text('wiki') | + | div[data-testid^='workspace-']:has-text('SubWiki') | + When I click on a "default wiki workspace button" element with selector "div[data-testid^='workspace-']:has-text('wiki')" + Then the browser view should be loaded and visible + And I wait for SSE and watch-fs to be ready + # Create tiddler with tag to test file system plugin + And I click on "add tiddler button" element in browser view with selector "button:has(.tc-image-new-button)" + # Focus on title input, clear it, and type new title in the draft tiddler + And I click on "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor" + And I wait for 0.2 seconds + And I press "Control+a" in browser view + And I wait for 0.2 seconds + And I press "Delete" in browser view + And I type "TestTiddlerTitle" in "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor" + # Wait for tiddler state to settle, otherwise it still shows 3 chars (新条目) for a while + And I wait for 2 seconds + Then I should see "16 chars" in the browser view content + # Input tag by typing in the tag input field - use precise selector to target the tag input specifically + And I click on "tag input" element in browser view with selector "div[data-tiddler-title^='Draft of'] div.tc-edit-add-tag-ui input.tc-edit-texteditor[placeholder='标签名称']" + And I wait for 0.2 seconds + And I press "Control+a" in browser view + And I wait for 0.2 seconds + And I press "Delete" in browser view + And I wait for 0.2 seconds + And I type "TestTag" in "tag input" element in browser view with selector "div[data-tiddler-title^='Draft of'] div.tc-edit-add-tag-ui input.tc-edit-texteditor[placeholder='标签名称']" + # Click the add tag button to confirm the tag (not just typing) + And I wait for 0.2 seconds + And I click on "add tag button" element in browser view with selector "div[data-tiddler-title^='Draft of'] span.tc-add-tag-button button" + # Wait for file system plugin to save the draft tiddler to SubWiki folder, Even 3 second will randomly failed in next step. + And I wait for 4.5 seconds + # Verify the DRAFT tiddler has been routed to sub-wiki immediately after adding the tag + Then file "Draft of '新条目'.tid" should exist in "{tmpDir}/SubWiki" + # Verify the draft file is NOT in main wiki tiddlers folder (it should have been moved to SubWiki) + Then file "Draft of '新条目'.tid" should not exist in "{tmpDir}/wiki/tiddlers" + # Click confirm button to save the tiddler + And I click on "confirm button" element in browser view with selector "button:has(.tc-image-done-button)" + And I wait for 1 seconds + # Verify the final tiddler file exists in sub-wiki folder after save + # After confirming the draft, it should be saved as TestTiddlerTitle.tid in SubWiki + Then file "TestTiddlerTitle.tid" should exist in "{tmpDir}/SubWiki" + # Test SSE is still working after SubWiki creation - modify a main wiki tiddler + When I modify file "{tmpDir}/wiki/tiddlers/Index.tid" to contain "Main wiki content modified after SubWiki creation" + Then I wait for tiddler "Index" to be updated by watch-fs + # Confirm Index always open + Then I should see a "Index tiddler" element in browser view with selector "div[data-tiddler-title='Index']" + Then I should see "Main wiki content modified after SubWiki creation" in the browser view content + # Test modification in sub-wiki folder - tiddler was routed there by tag + # Modify the tiddler file externally - need to preserve .tid format with metadata + When I modify file "{tmpDir}/SubWiki/TestTiddlerTitle.tid" to contain "Content modified in SubWiki folder" + # Wait for watch-fs to detect the change - use longer wait and open tiddler directly + And I wait for 2 seconds for "watch-fs to detect file change in sub-wiki" + # Open the tiddler directly to verify content was updated + When I open tiddler "TestTiddlerTitle" in browser view + And I wait for 1 seconds + # Verify the modified content appears in the wiki + Then I should see "Content modified in SubWiki folder" in the browser view content + + @subwiki @subwiki-load + Scenario: Sub-wiki tiddlers are loaded on initial wiki startup + # Setup: Create sub-wiki folder and settings BEFORE launching the app + Given I cleanup test wiki so it could create a new one on start + And I setup a sub-wiki "SubWikiPreload" with tag "PreloadTag" and tiddlers: + | title | tags | content | + | PreExistingTiddler | PreloadTag | Content from pre-existing sub-wiki tiddler | + # Now launch the app - it should load both main wiki and sub-wiki tiddlers + When I launch the TidGi application + And I wait for the page to load completely + Then I should see "page body and workspaces" elements with selectors: + | div[data-testid^='workspace-']:has-text('wiki') | + | div[data-testid^='workspace-']:has-text('SubWikiPreload') | + When I click on a "default wiki workspace button" element with selector "div[data-testid^='workspace-']:has-text('wiki')" + Then the browser view should be loaded and visible + And I wait for SSE and watch-fs to be ready + # Open the tiddler directly using TiddlyWiki API + When I open tiddler "PreExistingTiddler" in browser view + And I wait for 0.5 seconds + # Verify the tiddler content is displayed + Then I should see "Content from pre-existing sub-wiki tiddler" in the browser view content + # Verify the tiddler has the correct tag + Then I should see a "PreloadTag tag" element in browser view with selector "[data-tiddler-title='PreExistingTiddler'] [data-tag-title='PreloadTag']" + + @subwiki @subwiki-tagtree + Scenario: Tiddlers matching tag tree are routed to sub-wiki with includeTagTree enabled + # Setup: Create sub-wiki with includeTagTree enabled, and pre-existing tag hierarchy A->B + # TagTreeRoot is the sub-wiki's tagName + # TiddlerA has tag "TagTreeRoot" (direct child) + # TiddlerB has tag "TiddlerA" (grandchild via tag tree) + Given I cleanup test wiki so it could create a new one on start + And I setup a sub-wiki "SubWikiTagTree" with tag "TagTreeRoot" and includeTagTree enabled and tiddlers: + | title | tags | content | + | TiddlerA | TagTreeRoot | TiddlerA with TagTreeRoot tag | + | TiddlerB | TiddlerA | TiddlerB with TiddlerA tag | + # Now launch the app + When I launch the TidGi application + And I wait for the page to load completely + Then I should see "page body and workspaces" elements with selectors: + | div[data-testid^='workspace-']:has-text('wiki') | + | div[data-testid^='workspace-']:has-text('SubWikiTagTree') | + When I click on a "default wiki workspace button" element with selector "div[data-testid^='workspace-']:has-text('wiki')" + Then the browser view should be loaded and visible + And I wait for SSE and watch-fs to be ready + # Verify TiddlerA and TiddlerB were loaded from sub-wiki by opening them + When I open tiddler "TiddlerA" in browser view + And I wait for 0.5 seconds + Then I should see "TiddlerA with TagTreeRoot tag" in the browser view content + When I open tiddler "TiddlerB" in browser view + And I wait for 0.5 seconds + Then I should see "TiddlerB with TiddlerA tag" in the browser view content + # Now create TiddlerC with tag TiddlerB (testing tag tree routing: TiddlerC -> TiddlerB -> TiddlerA -> TagTreeRoot) + And I click on "add tiddler button" element in browser view with selector "button:has(.tc-image-new-button)" + And I click on "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor" + And I wait for 0.2 seconds + And I press "Control+a" in browser view + And I wait for 0.2 seconds + And I press "Delete" in browser view + And I type "TiddlerC" in "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor" + And I wait for 0.5 seconds + # Add TiddlerB as a tag (testing tag tree traversal: TiddlerC -> TiddlerB -> TiddlerA -> TagTreeRoot) + And I click on "tag input" element in browser view with selector "div[data-tiddler-title^='Draft of'] div.tc-edit-add-tag-ui input.tc-edit-texteditor[placeholder='标签名称']" + And I wait for 0.2 seconds + And I type "TiddlerB" in "tag input" element in browser view with selector "div[data-tiddler-title^='Draft of'] div.tc-edit-add-tag-ui input.tc-edit-texteditor[placeholder='标签名称']" + And I wait for 0.2 seconds + And I click on "add tag button" element in browser view with selector "div[data-tiddler-title^='Draft of'] span.tc-add-tag-button button" + And I wait for 0.5 seconds + And I click on "confirm button" element in browser view with selector "button:has(.tc-image-done-button)" + And I wait for 3 seconds for "TiddlerC to be saved via tag tree routing" + # Verify TiddlerC is saved to sub-wiki via tag tree (TiddlerB -> TiddlerA -> TagTreeRoot) + # This confirms in-tagtree-of filter is working correctly + Then file "TiddlerC.tid" should exist in "{tmpDir}/SubWikiTagTree" + # Verify that TiddlerC is NOT in main wiki tiddlers folder + Then file "TiddlerC.tid" should not exist in "{tmpDir}/wiki/tiddlers" + + @subwiki @subwiki-filter + Scenario: Tiddlers matching custom filter are routed to sub-wiki + # Setup: Create sub-wiki with custom filter that matches tiddlers with "FilterTest" field + # The filter "[has[filtertest]]" will match any tiddler with a "filtertest" field + Given I cleanup test wiki so it could create a new one on start + And I setup a sub-wiki "SubWikiFilter" with tag "FilterTag" and filter "[has[filtertest]]" and tiddlers: + | title | tags | content | + | FilterTiddlerA | FilterTag | TiddlerA matched by filter | + # Now launch the app + When I launch the TidGi application + And I wait for the page to load completely + Then I should see "page body and workspaces" elements with selectors: + | div[data-testid^='workspace-']:has-text('wiki') | + | div[data-testid^='workspace-']:has-text('SubWikiFilter') | + When I click on a "default wiki workspace button" element with selector "div[data-testid^='workspace-']:has-text('wiki')" + Then the browser view should be loaded and visible + And I wait for SSE and watch-fs to be ready + # Create a tiddler with the "filtertest" field to test filter routing + And I click on "add tiddler button" element in browser view with selector "button:has(.tc-image-new-button)" + And I click on "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor" + And I wait for 0.2 seconds + And I press "Control+a" in browser view + And I wait for 0.2 seconds + And I press "Delete" in browser view + And I type "FilterMatchTiddler" in "title input" element in browser view with selector "div[data-tiddler-title^='Draft of'] input.tc-titlebar.tc-edit-texteditor" + And I wait for 0.5 seconds + # Add the "filtertest" field by clicking on add field button + And I click on "add field name input" element in browser view with selector "div[data-tiddler-title^='Draft of'] .tc-edit-field-add-name-wrapper input" + And I wait for 0.2 seconds + And I type "filtertest" in "add field name input" element in browser view with selector "div[data-tiddler-title^='Draft of'] .tc-edit-field-add-name-wrapper input" + And I wait for 0.2 seconds + And I click on "add field value input" element in browser view with selector "div[data-tiddler-title^='Draft of'] .tc-edit-field-add-value input" + And I wait for 0.2 seconds + And I type "yes" in "add field value input" element in browser view with selector "div[data-tiddler-title^='Draft of'] .tc-edit-field-add-value input" + And I wait for 0.2 seconds + And I click on "add field button" element in browser view with selector "div[data-tiddler-title^='Draft of'] .tc-edit-field-add button" + And I wait for 0.5 seconds + # Confirm to save the tiddler + And I click on "confirm button" element in browser view with selector "button:has(.tc-image-done-button)" + And I wait for 3 seconds for "FilterMatchTiddler to be saved via filter routing" + # Verify FilterMatchTiddler is saved to sub-wiki via filter + Then file "FilterMatchTiddler.tid" should exist in "{tmpDir}/SubWikiFilter" + # Verify that FilterMatchTiddler is NOT in main wiki tiddlers folder + Then file "FilterMatchTiddler.tid" should not exist in "{tmpDir}/wiki/tiddlers" + + @subwiki @subwiki-settings-ui + Scenario: Sub-wiki settings UI can enable includeTagTree option + # This tests the EditWorkspace UI for setting includeTagTree via the new switch + Given I cleanup test wiki so it could create a new one on start + And I setup a sub-wiki "SubWikiSettings" with tag "SettingsTag" and tiddlers: + | title | tags | content | + | SettingsTiddler | SettingsTag | Settings test tiddler | + When I launch the TidGi application + And I wait for the page to load completely + Then I should see "page body and workspaces" elements with selectors: + | div[data-testid^='workspace-']:has-text('wiki') | + | div[data-testid^='workspace-']:has-text('SubWikiSettings') | + # Open the edit workspace window using existing step + When I open edit workspace window for workspace with name "SubWikiSettings" + And I switch to "editWorkspace" window + And I wait for the page to load completely + And I wait for 1 seconds for "page to fully render" + # For sub-wikis, the accordion is defaultExpanded, so we should see the switch immediately + Then I should see a "sub-workspace options accordion" element with selector "[data-testid='preference-section-subWorkspaceOptions']" + # The includeTagTree switch should be visible for sub-wikis (accordion is already expanded) + Then I should see a "includeTagTree switch" element with selector "[data-testid='include-tag-tree-switch']" + # Enable includeTagTree option + When I click on a "includeTagTree switch" element with selector "[data-testid='include-tag-tree-switch']" + And I wait for 0.5 seconds + # Save the changes by clicking the save button + When I click on a "save button" element with selector "[data-testid='edit-workspace-save-button']" + Then I should not see a "save button" element with selector "[data-testid='edit-workspace-save-button']" + And I wait for 0.5 seconds for "settings to be written" + # Verify the setting was saved to settings.json + Then settings.json should have workspace "SubWikiSettings" with "includeTagTree" set to "true" + + @subwiki @subwiki-create-ui + Scenario: Create sub-wiki workspace via UI + # This tests creating a sub-wiki through the Add Workspace UI + Given I cleanup test wiki so it could create a new one on start + And I launch the TidGi application + And I wait for the page to load completely + Then I should see a "default wiki workspace" element with selector "div[data-testid^='workspace-']:has-text('wiki')" + When I click on a "default wiki workspace button" element with selector "div[data-testid^='workspace-']:has-text('wiki')" + Then the browser view should be loaded and visible + And I wait for SSE and watch-fs to be ready + # Create sub-workspace via UI + When I click on an "add workspace button" element with selector "#add-workspace-button" + And I switch to "addWorkspace" window + # Toggle to sub-workspace mode by clicking the switch + And I click on a "main/sub workspace switch" element with selector "[data-testid='main-sub-workspace-switch']" + # Select the first (default) wiki workspace from dropdown + And I select "wiki" from MUI Select with test id "main-wiki-select" + # Type folder name + And I type "SubWikiUI" in "sub wiki folder name input" element with selector "input[aria-describedby*='-helper-text'][value='wiki']" + # Add tag using Autocomplete - type and press Enter to add the tag + And I type "UITestTag" in "tag name input" element with selector "[data-testid='tagname-autocomplete-input']" + And I press "Enter" key + And I click on a "create sub workspace button" element with selector "button.MuiButton-colorSecondary" + And I switch to "main" window + Then I should see a "SubWikiUI workspace" element with selector "div[data-testid^='workspace-']:has-text('SubWikiUI')" + # Wait for main wiki to restart after sub-wiki creation + Then I wait for "main wiki restarted after sub-wiki creation" log marker "[test-id-MAIN_WIKI_RESTARTED_AFTER_SUBWIKI]" + And I wait for "watch-fs stabilized after restart" log marker "[test-id-WATCH_FS_STABILIZED]" + And I wait for "SSE ready after restart" log marker "[test-id-SSE_READY]" + Then I wait for "view loaded" log marker "[test-id-VIEW_LOADED]" + # Wait for TiddlyWiki to fully render the page (site title appears) + Then I wait for "site title" element in browser view with selector "h1.tc-site-title" + # Click SubWikiUI workspace to see the missing tag tiddler message + When I click on a "SubWikiUI workspace button" element with selector "div[data-testid^='workspace-']:has-text('SubWikiUI')" + # Verify UITestTag text is visible (missing tiddler message shows the title) + Then I should see "UITestTag" in the browser view content + # Verify the sub-wiki was created in settings.json + Then settings.json should have workspace "SubWikiUI" with "tagNames" containing "UITestTag" diff --git a/features/supports/webContentsViewHelper.ts b/features/supports/webContentsViewHelper.ts index 790d528a..53f42b0f 100644 --- a/features/supports/webContentsViewHelper.ts +++ b/features/supports/webContentsViewHelper.ts @@ -346,3 +346,30 @@ export async function captureScreenshot(app: ElectronApplication, screenshotPath return false; } } + +/** + * Execute TiddlyWiki code in the browser view + * Useful for directly manipulating the wiki, e.g., opening tiddlers + */ +export async function executeTiddlyWikiCode( + app: ElectronApplication, + code: string, +): Promise { + const webContentsId = await getFirstWebContentsView(app); + + if (!webContentsId) { + throw new Error('No WebContentsView found'); + } + + return await app.evaluate( + async ({ webContents }, [id, codeContent]) => { + const targetWebContents = webContents.fromId(id as number); + if (!targetWebContents) { + throw new Error('WebContents not found'); + } + const result: T = await targetWebContents.executeJavaScript(codeContent as string, true) as T; + return result; + }, + [webContentsId, code], + ); +} diff --git a/src/constants/paths.ts b/src/constants/paths.ts index 4429d03c..2f3ec36d 100644 --- a/src/constants/paths.ts +++ b/src/constants/paths.ts @@ -81,9 +81,9 @@ export const LOCALIZATION_FOLDER = isPackaged // Default wiki locations // For E2E tests, always use project root's wiki-test (outside asar) -// process.resourcesPath: out/TidGi-.../resources -> need ../../.. to get to project root +// process.resourcesPath: out/TidGi-darwin-x64/TidGi.app/Contents/Resources -> need 5x .. to get to project root export const DEFAULT_FIRST_WIKI_FOLDER_PATH = isTest && isPackaged - ? path.resolve(process.resourcesPath, '..', '..', '..', testWikiFolderName) // E2E packaged: project root + ? path.resolve(process.resourcesPath, '..', '..', '..', '..', '..', testWikiFolderName) // E2E packaged: project root : isTest ? path.resolve(__dirname, '..', '..', testWikiFolderName) // E2E dev: project root : isDevelopmentOrTest diff --git a/src/windows/EditWorkspace/index.tsx b/src/windows/EditWorkspace/index.tsx index 31de3b63..ea9a7a7f 100644 --- a/src/windows/EditWorkspace/index.tsx +++ b/src/windows/EditWorkspace/index.tsx @@ -460,6 +460,7 @@ export default function EditWorkspace(): React.JSX.Element { edge='end' color='primary' checked={includeTagTree} + data-testid='include-tag-tree-switch' onChange={(event: React.ChangeEvent) => { workspaceSetter({ ...workspace, includeTagTree: event.target.checked }, true); }}