From 0ee7b634eda662fdb03f5ff087efd564ada4b31b Mon Sep 17 00:00:00 2001 From: lin onetwo Date: Thu, 30 Oct 2025 13:57:23 +0800 Subject: [PATCH] feat: capture webview screenshot --- features/stepDefinitions/application.ts | 16 +++++-- features/supports/webContentsViewHelper.ts | 44 +++++++++++++++++++ .../wiki/wikiWorker/ipcServerRoutes.ts | 12 ++--- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/features/stepDefinitions/application.ts b/features/stepDefinitions/application.ts index 7981a506..299bf412 100644 --- a/features/stepDefinitions/application.ts +++ b/features/stepDefinitions/application.ts @@ -9,6 +9,7 @@ import { MockOAuthServer } from '../supports/mockOAuthServer'; import { MockOpenAIServer } from '../supports/mockOpenAI'; import { makeSlugPath, screenshotsDirectory } from '../supports/paths'; import { getPackedAppPath } from '../supports/paths'; +import { captureScreenshot } from '../supports/webContentsViewHelper'; // Backoff configuration for retries const BACKOFF_OPTIONS = { @@ -200,7 +201,7 @@ AfterStep(async function(this: ApplicationWorld, { pickle, pickleStep, result }) try { const stepText = pickleStep.text; - + // Skip screenshots for wait steps to avoid too many screenshots if (stepText.match(/^I wait for \d+(\.\d+)? seconds?$/i)) { return; @@ -244,10 +245,17 @@ AfterStep(async function(this: ApplicationWorld, { pickle, pickleStep, result }) } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const screenshotPath = path.resolve(featureDirectory, `${timestamp}-${cleanStepText}-${stepStatus}.jpg`); + + // Try to capture both WebContentsView and Page screenshots + let webViewCaptured = false; + if (this.app) { + const webViewScreenshotPath = path.resolve(featureDirectory, `${timestamp}-${cleanStepText}-${stepStatus}-webview.png`); + webViewCaptured = await captureScreenshot(this.app, webViewScreenshotPath); + } - // Use conservative screenshot options for CI - await pageToUse.screenshot({ path: screenshotPath, fullPage: true, type: 'jpeg', quality: 10 }); + // Always capture page screenshot (UI chrome/window) + const pageScreenshotPath = path.resolve(featureDirectory, `${timestamp}-${cleanStepText}-${stepStatus}${webViewCaptured ? '-page' : ''}.png`); + await pageToUse.screenshot({ path: pageScreenshotPath, fullPage: true, type: 'png' }); } catch (screenshotError) { console.warn('Failed to take screenshot:', screenshotError); } diff --git a/features/supports/webContentsViewHelper.ts b/features/supports/webContentsViewHelper.ts index bee7144b..64f947f0 100644 --- a/features/supports/webContentsViewHelper.ts +++ b/features/supports/webContentsViewHelper.ts @@ -1,4 +1,5 @@ import { WebContentsView } from 'electron'; +import fs from 'fs-extra'; import type { ElectronApplication } from 'playwright'; /** @@ -288,3 +289,46 @@ export async function elementExists(app: ElectronApplication, selector: string): return false; } } + +/** + * Capture screenshot of WebContentsView + * Returns true if screenshot was taken successfully, false if WebContentsView not found + */ +export async function captureScreenshot(app: ElectronApplication, screenshotPath: string): Promise { + try { + const webContentsId = await getFirstWebContentsView(app); + + if (!webContentsId) { + return false; + } + + const pngBufferData = await app.evaluate( + async ({ webContents }, id: number) => { + const targetWebContents = webContents.fromId(id); + if (!targetWebContents) { + return null; + } + + try { + const image = await targetWebContents.capturePage(); + const pngBuffer = image.toPNG(); + return Array.from(pngBuffer); + } catch (error) { + console.error('Failed to capture screenshot:', error); + return null; + } + }, + webContentsId, + ); + + if (!pngBufferData) { + return false; + } + + await fs.writeFile(screenshotPath, Buffer.from(pngBufferData)); + return true; + } catch (error) { + console.error('Error capturing screenshot:', error); + return false; + } +} diff --git a/src/services/wiki/wikiWorker/ipcServerRoutes.ts b/src/services/wiki/wikiWorker/ipcServerRoutes.ts index 84334dc0..f6e18264 100644 --- a/src/services/wiki/wikiWorker/ipcServerRoutes.ts +++ b/src/services/wiki/wikiWorker/ipcServerRoutes.ts @@ -185,16 +185,16 @@ export class IpcServerRoutes { } } tiddlerFieldsToPut.title = title; - + // Mark this tiddler as recently saved to prevent echo this.recentlySavedTiddlers.add(title); - + this.wikiInstance.wiki.addTiddler(new this.wikiInstance.Tiddler(tiddlerFieldsToPut)); - + // Note: The change event is triggered synchronously by addTiddler // The event handler in getWikiChangeObserver$ will check recentlySavedTiddlers // and remove the mark after filtering - + const changeCount = this.wikiInstance.wiki.getChangeCount(title).toString(); return { statusCode: 204, headers: { 'Content-Type': 'text/plain', Etag: `"default/${encodeURIComponent(title)}/${changeCount}:"` }, data: 'OK' }; } @@ -239,7 +239,7 @@ export class IpcServerRoutes { // Filter out tiddlers that were just saved via IPC to prevent echo const filteredChanges: IChangedTiddlers = {}; let hasChanges = false; - + for (const title in changes) { if (this.recentlySavedTiddlers.has(title)) { // This change was caused by our own putTiddler, skip it to prevent echo @@ -249,7 +249,7 @@ export class IpcServerRoutes { filteredChanges[title] = changes[title]; hasChanges = true; } - + // Only notify if there are actual changes after filtering if (hasChanges) { observer.next(filteredChanges);