test: refactor subwiki test logic to a file

This commit is contained in:
linonetwo 2025-12-06 01:17:10 +08:00
parent d76b3ff098
commit dda10b25fb
7 changed files with 632 additions and 85 deletions

View file

@ -12,83 +12,6 @@ Feature: Filesystem Plugin
Then the browser view should be loaded and visible Then the browser view should be loaded and visible
And I wait for SSE and watch-fs to be ready 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 @file-watching
Scenario: External file creation syncs to wiki Scenario: External file creation syncs to wiki
# Create a test tiddler file directly on filesystem # Create a test tiddler file directly on filesystem

View file

@ -1,6 +1,6 @@
import { Then, When } from '@cucumber/cucumber'; import { Then, When } from '@cucumber/cucumber';
import { backOff } from 'exponential-backoff'; 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'; import type { ApplicationWorld } from './application';
// Backoff configuration for retries // 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) { When('I click on {string} element in browser view with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
if (!this.app) { if (!this.app) {
throw new Error('Application not launched'); 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`); 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}`);
}
});

View file

@ -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 { exec as gitExec } from 'dugite';
import { backOff } from 'exponential-backoff'; import { backOff } from 'exponential-backoff';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import type { IWorkspace } from '../../src/services/workspaces/interface'; 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'; import type { ApplicationWorld } from './application';
// Backoff configuration for retries // 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() { 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); 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. * 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], * 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<string, IWorkspace> = {}; const filtered: Record<string, IWorkspace> = {};
for (const id of Object.keys(workspaces)) { for (const id of Object.keys(workspaces)) {
const ws = workspaces[id]; const ws = workspaces[id];
const name = ws.name; // Keep only page-type workspaces (agent, help, guide, add), remove all wiki workspaces
if (name === 'wiki' || id === 'wiki') continue; // This includes main wiki and all sub-wikis
filtered[id] = ws; if ('pageType' in ws && ws.pageType) {
filtered[id] = ws;
}
} }
// Write with exponential backoff retry logic to handle file locks // 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<string, IWorkspace> } & Record<string, unknown>;
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<string, unknown>)[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<string, IWorkspace> } & Record<string, unknown>;
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<string, unknown>)[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 * Cleanup function for sub-wiki routing test
* Removes test workspaces created during the 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<string, string>[],
) {
// 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<string, IWorkspace> } & Record<string, unknown> = {};
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 }; export { clearGitTestData, clearHibernationTestData, clearSubWikiRoutingTestData, clearTestIdLogs };
/** /**

263
features/subWiki.feature Normal file
View file

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

View file

@ -346,3 +346,30 @@ export async function captureScreenshot(app: ElectronApplication, screenshotPath
return false; return false;
} }
} }
/**
* Execute TiddlyWiki code in the browser view
* Useful for directly manipulating the wiki, e.g., opening tiddlers
*/
export async function executeTiddlyWikiCode<T>(
app: ElectronApplication,
code: string,
): Promise<T | null> {
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],
);
}

View file

@ -81,9 +81,9 @@ export const LOCALIZATION_FOLDER = isPackaged
// Default wiki locations // Default wiki locations
// For E2E tests, always use project root's wiki-test (outside asar) // 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 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 : isTest
? path.resolve(__dirname, '..', '..', testWikiFolderName) // E2E dev: project root ? path.resolve(__dirname, '..', '..', testWikiFolderName) // E2E dev: project root
: isDevelopmentOrTest : isDevelopmentOrTest

View file

@ -460,6 +460,7 @@ export default function EditWorkspace(): React.JSX.Element {
edge='end' edge='end'
color='primary' color='primary'
checked={includeTagTree} checked={includeTagTree}
data-testid='include-tag-tree-switch'
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
workspaceSetter({ ...workspace, includeTagTree: event.target.checked }, true); workspaceSetter({ ...workspace, includeTagTree: event.target.checked }, true);
}} }}