Refactor and improve external URL checks
- Move external URL checks to its own module under `tests/`. This separates them from integration test, addressing long runs and frequent failures that led to ignoring test results. - Move `check-desktop-runtime-errors` to `tests/checks` to keep all test-related checks into one directory. - Replace `ts-node` with `vite` for running `check-desktop-runtime-errors` to maintain a consistent execution environment across checks. - Implement a timeout for each fetch call. - Be nice to external sources, wait 5 seconds before sending another request to an URL under same domain. This solves rate-limiting issues. - Instead of running test on every push/pull request, run them only weekly. - Do not run tests on each commit/PR but only scheduled (weekly) to minimize noise. - Fix URLs are not captured correctly inside backticks or parenthesis.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { unlink } from 'fs/promises';
|
||||
import { runCommand } from '../../utils/run-command';
|
||||
import { log, LogLevel } from '../../utils/log';
|
||||
import { CURRENT_PLATFORM, SupportedPlatform } from '../../utils/platform';
|
||||
import { exists } from '../../utils/io';
|
||||
|
||||
export async function captureScreen(
|
||||
imagePath: string,
|
||||
): Promise<void> {
|
||||
if (!imagePath) {
|
||||
throw new Error('Path for screenshot not provided');
|
||||
}
|
||||
|
||||
if (await exists(imagePath)) {
|
||||
log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LogLevel.Warn);
|
||||
unlink(imagePath);
|
||||
}
|
||||
|
||||
const platformCommands: {
|
||||
readonly [K in SupportedPlatform]: string;
|
||||
} = {
|
||||
[SupportedPlatform.macOS]: `screencapture -x ${imagePath}`,
|
||||
[SupportedPlatform.Linux]: `import -window root ${imagePath}`,
|
||||
[SupportedPlatform.Windows]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`,
|
||||
};
|
||||
|
||||
const commandForPlatform = platformCommands[CURRENT_PLATFORM];
|
||||
|
||||
if (!commandForPlatform) {
|
||||
log(`Screenshot capture not supported on: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn);
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Capturing screenshot to ${imagePath} using command:\n\t> ${commandForPlatform}`);
|
||||
|
||||
const { error } = await runCommand(commandForPlatform);
|
||||
if (error) {
|
||||
log(`Failed to capture screenshot.\n${error}`, LogLevel.Warn);
|
||||
return;
|
||||
}
|
||||
log(`Captured screenshot to ${imagePath}.`);
|
||||
}
|
||||
|
||||
function getScreenshotPowershellScript(imagePath: string): string {
|
||||
return `
|
||||
$ProgressPreference = 'SilentlyContinue' # Do not pollute stderr
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
||||
|
||||
$bmp = New-Object System.Drawing.Bitmap $screenBounds.Width, $screenBounds.Height
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$graphics.CopyFromScreen([System.Drawing.Point]::Empty, [System.Drawing.Point]::Empty, $screenBounds.Size)
|
||||
|
||||
$bmp.Save('${imagePath}')
|
||||
$graphics.Dispose()
|
||||
$bmp.Dispose()
|
||||
`;
|
||||
}
|
||||
|
||||
function encodeForPowershell(script: string): string {
|
||||
const buffer = Buffer.from(script, 'utf16le');
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { runCommand } from '../../utils/run-command';
|
||||
import { log, LogLevel } from '../../utils/log';
|
||||
import { SupportedPlatform, CURRENT_PLATFORM } from '../../utils/platform';
|
||||
|
||||
export async function captureWindowTitles(processId: number) {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
|
||||
const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM];
|
||||
if (!captureFunction) {
|
||||
log(`Cannot capture window title, unsupported OS: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return captureFunction(processId);
|
||||
}
|
||||
|
||||
const windowTitleCaptureFunctions: {
|
||||
readonly [K in SupportedPlatform]: (processId: number) => Promise<string[]>;
|
||||
} = {
|
||||
[SupportedPlatform.macOS]: (processId) => captureTitlesOnMac(processId),
|
||||
[SupportedPlatform.Linux]: (processId) => captureTitlesOnLinux(processId),
|
||||
[SupportedPlatform.Windows]: (processId) => captureTitlesOnWindows(processId),
|
||||
};
|
||||
|
||||
async function captureTitlesOnWindows(processId: number): Promise<string[]> {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
|
||||
const { stdout: tasklistOutput, error } = await runCommand(
|
||||
`tasklist /FI "PID eq ${processId}" /fo list /v`,
|
||||
);
|
||||
if (error) {
|
||||
log(`Failed to retrieve window title.\n${error}`, LogLevel.Warn);
|
||||
return [];
|
||||
}
|
||||
const regex = /Window Title:\s*(.*)/;
|
||||
const match = regex.exec(tasklistOutput);
|
||||
if (match && match.length > 1 && match[1]) {
|
||||
const title = match[1].trim();
|
||||
if (title === 'N/A') {
|
||||
return [];
|
||||
}
|
||||
return [title];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function captureTitlesOnLinux(processId: number): Promise<string[]> {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
|
||||
const { stdout: windowIdsOutput, error: windowIdError } = await runCommand(
|
||||
`xdotool search --pid '${processId}'`,
|
||||
);
|
||||
|
||||
if (windowIdError || !windowIdsOutput) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const windowIds = windowIdsOutput.trim().split('\n');
|
||||
|
||||
const titles = await Promise.all(windowIds.map(async (windowId) => {
|
||||
const { stdout: titleOutput, error: titleError } = await runCommand(
|
||||
`xprop -id ${windowId} | grep "WM_NAME(STRING)" | cut -d '=' -f 2 | sed 's/^[[:space:]]*"\\(.*\\)"[[:space:]]*$/\\1/'`,
|
||||
);
|
||||
if (titleError || !titleOutput) {
|
||||
return undefined;
|
||||
}
|
||||
return titleOutput.trim();
|
||||
}));
|
||||
|
||||
return titles.filter(Boolean);
|
||||
}
|
||||
|
||||
let hasAssistiveAccessOnMac = true;
|
||||
|
||||
async function captureTitlesOnMac(processId: number): Promise<string[]> {
|
||||
if (!processId) { throw new Error('Missing process ID.'); }
|
||||
if (!hasAssistiveAccessOnMac) {
|
||||
return [];
|
||||
}
|
||||
const script = `
|
||||
tell application "System Events"
|
||||
try
|
||||
set targetProcess to first process whose unix id is ${processId}
|
||||
on error
|
||||
return
|
||||
end try
|
||||
tell targetProcess
|
||||
set allWindowNames to {}
|
||||
repeat with aWindow in windows
|
||||
set end of allWindowNames to name of aWindow
|
||||
end repeat
|
||||
return allWindowNames
|
||||
end tell
|
||||
end tell
|
||||
`;
|
||||
const argument = script.trim()
|
||||
.split(/[\r\n]+/)
|
||||
.map((line) => `-e '${line.trim()}'`)
|
||||
.join(' ');
|
||||
|
||||
const { stdout: titleOutput, error } = await runCommand(`osascript ${argument}`);
|
||||
if (error) {
|
||||
let errorMessage = '';
|
||||
if (error.includes('-25211')) {
|
||||
errorMessage += 'Capturing window title requires assistive access. You do not have it.\n';
|
||||
hasAssistiveAccessOnMac = false;
|
||||
}
|
||||
errorMessage += error;
|
||||
log(errorMessage, LogLevel.Warn);
|
||||
return [];
|
||||
}
|
||||
const title = titleOutput?.trim();
|
||||
if (!title) {
|
||||
return [];
|
||||
}
|
||||
return [title];
|
||||
}
|
||||
Reference in New Issue
Block a user