TidGi-Desktop/features/stepDefinitions/cleanup.ts
linonetwo e024b7e65b fix(workspace-group): resolve E2E drag-and-drop crashes and failures
- Memoize sortable data to prevent dnd-kit re-registration loops when
  workspaces$ emits new object references (#900)
- Deduplicate workspaces$ and groups$ emissions in main process to
  suppress unnecessary renderer re-renders
- Debounce drag state updates to avoid React Maximum update depth
  exceeded errors during rapid onDragMove/onDragOver events
- Fix customCollisionDetection priority so own group header outranks
  nearby workspace collisions, enabling ungroup on header drop
- Stabilize allDraggableIds during drag by deriving from canonical
  order only, avoiding SortableContext re-registration loops
- Clear workspaceGroups during E2E test cleanup to prevent stale
  groups from previous runs
- Add pageerror/console.error capture in E2E drag helpers for
  diagnosing renderer crashes
2026-04-27 08:01:23 +08:00

126 lines
4.7 KiB
TypeScript

import { After, Before } from '@cucumber/cucumber';
import fs from 'fs-extra';
import path from 'path';
import { makeSlugPath } from '../supports/paths';
import { clearAISettings } from './agent';
import { ApplicationWorld } from './application';
import { clearTidgiMiniWindowSettings } from './tidgiMiniWindow';
import { clearHibernationTestData, clearSubWikiRoutingTestData } from './wiki';
Before(async function(this: ApplicationWorld, { pickle }) {
// Initialize scenario-specific paths
this.scenarioName = pickle.name;
this.scenarioSlug = makeSlugPath(pickle.name, 60);
this.scenarioTags = pickle.tags.map((tag) => tag.name);
const scenarioRoot = path.resolve(process.cwd(), 'test-artifacts', this.scenarioSlug);
const logsDirectory = path.resolve(scenarioRoot, 'userData-test', 'logs');
const screenshotsDirectory = path.resolve(logsDirectory, 'screenshots');
const wikiTestRoot = path.resolve(scenarioRoot, 'wiki-test');
// Create necessary directories for this scenario
await fs.ensureDir(logsDirectory);
await fs.ensureDir(screenshotsDirectory);
await fs.ensureDir(wikiTestRoot); // Ensure wiki-test root exists for default wiki creation
if (pickle.tags.some((tag) => tag.name === '@ai-setting')) {
await clearAISettings(scenarioRoot);
}
if (pickle.tags.some((tag) => tag.name === '@tidgi-mini-window')) {
await clearTidgiMiniWindowSettings(scenarioRoot);
}
});
After(async function(this: ApplicationWorld, { pickle }) {
// IMPORTANT: Close app FIRST before cleaning up files
// This releases file locks so wiki folders can be deleted
if (this.app) {
try {
// Close all windows including tidgi mini window before closing the app, otherwise it might hang, and refused to exit until ctrl+C
const allWindows = this.app.windows();
// Try to close windows gracefully with short timeout, then force close
await Promise.allSettled(
allWindows.map(async (window) => {
if (window.isClosed()) return;
try {
// Very short timeout for window close - we'll force close anyway
await Promise.race([
window.close(),
new Promise((_, reject) =>
setTimeout(() => {
reject(new Error('Window close timeout'));
}, 1000)
),
]);
} catch {
// Window close failed or timed out, ignore and continue
// Force close will happen at app level
}
}),
);
// Try to close app gracefully with short timeout
try {
await Promise.race([
this.app.close(),
new Promise((_, reject) =>
setTimeout(() => {
reject(new Error('App close timeout'));
}, 1000)
),
]);
} catch {
// App close failed or timed out, force close immediately
}
} catch {
// Any error in the try block, continue to force close
} finally {
// ALWAYS force close, regardless of success/failure above
// This ensures resources are freed even if graceful close hangs
try {
if (this.app) {
// Force close browser context - this kills all processes
await Promise.race([
this.app.context().close({ reason: 'Force cleanup after test' }),
new Promise((resolve) => setTimeout(resolve, 500)), // 500ms max for force close
]);
}
} catch {
// Even force close can fail, but we don't care - move on
}
// Clear references immediately
this.app = undefined;
this.mainWindow = undefined;
this.currentWindow = undefined;
}
}
const scenarioRoot = path.resolve(process.cwd(), 'test-artifacts', this.scenarioSlug);
// Clean up settings and test data AFTER app is closed
if (pickle.tags.some((tag) => tag.name === '@tidgi-mini-window')) {
await clearTidgiMiniWindowSettings(scenarioRoot);
}
if (pickle.tags.some((tag) => tag.name === '@ai-setting')) {
await clearAISettings(scenarioRoot);
}
if (pickle.tags.some((tag) => tag.name === '@subwiki')) {
await clearSubWikiRoutingTestData(scenarioRoot);
}
// Clean up hibernation test data - remove wiki2 folder created during tests
if (pickle.tags.some((tag) => tag.name === '@hibernation')) {
await clearHibernationTestData(scenarioRoot);
}
// Clean up move workspace test data - remove wiki-test-moved folder
if (pickle.tags.some((tag) => tag.name === '@move-workspace')) {
const wikiTestMovedPath = path.resolve(scenarioRoot, 'wiki-test-moved');
if (await fs.pathExists(wikiTestMovedPath)) {
await fs.remove(wikiTestMovedPath);
}
}
// Scenario-specific logs are already in the right place, no need to move them
});