mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-06 02:30:47 -08:00
test: refactor subwiki test logic to a file
This commit is contained in:
parent
d76b3ff098
commit
dda10b25fb
7 changed files with 632 additions and 85 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,10 +124,12 @@ When('I cleanup test wiki so it could create a new one on start', async function
|
|||
const filtered: Record<string, IWorkspace> = {};
|
||||
for (const id of Object.keys(workspaces)) {
|
||||
const ws = workspaces[id];
|
||||
const name = ws.name;
|
||||
if (name === 'wiki' || id === 'wiki') continue;
|
||||
// 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
|
||||
try {
|
||||
|
|
@ -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
|
||||
* 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 };
|
||||
|
||||
/**
|
||||
|
|
|
|||
263
features/subWiki.feature
Normal file
263
features/subWiki.feature
Normal 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"
|
||||
|
|
@ -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<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],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<HTMLInputElement>) => {
|
||||
workspaceSetter({ ...workspace, includeTagTree: event.target.checked }, true);
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue