mirror of
https://github.com/D-Ogi/WatermarkRemover-AI.git
synced 2026-10-21 22:22:35 -05:00
168 lines
8.2 KiB
Python
168 lines
8.2 KiB
Python
"""Exercise the actual local desktop page, assets, configuration and API bridge."""
|
|
import json
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import threading
|
|
import tempfile
|
|
import time
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
from desktop_runtime import configure_runtime
|
|
configure_runtime()
|
|
logging.getLogger().addHandler(logging.StreamHandler())
|
|
import remwmgui
|
|
|
|
# The watchdog may terminate without finally blocks. Never let this diagnostic
|
|
# persist its probe settings in the user's actual configuration, even on timeout.
|
|
check_config = tempfile.TemporaryDirectory(prefix="wmr-desktop-check-")
|
|
remwmgui.CONFIG_FILE = str(Path(check_config.name) / "ui.yml")
|
|
|
|
finished = threading.Event()
|
|
failures = []
|
|
|
|
|
|
|
|
def check_sidebar_layout(window):
|
|
"""Measure actual rendered controls and tooltip bounds across all themes/languages."""
|
|
window.resize(800, 600)
|
|
window.evaluate_js("""window.sidebarProbe = null; (async () => {
|
|
const app = window.appInstance, rows = [];
|
|
for (const language of app.availableLanguages) {
|
|
app.t = await loadLanguage(language.id);
|
|
await Alpine.nextTick();
|
|
for (const theme of app.availableThemes) {
|
|
switchTheme(theme.id);
|
|
const aside = document.querySelector('aside');
|
|
void aside.offsetWidth;
|
|
await document.fonts.ready;
|
|
const bounds = aside.getBoundingClientRect();
|
|
const outside = [...aside.querySelectorAll('button, input, select')]
|
|
.filter(node => node.getClientRects().length)
|
|
.filter(node => node.getBoundingClientRect().right > bounds.right + 1)
|
|
.map(node => node.tagName);
|
|
// Invisible pseudo-elements still affect the scrollable width.
|
|
// The same box must remain inside when its tooltip becomes visible.
|
|
const hints = [...aside.querySelectorAll('[data-tooltip]')]
|
|
.filter(node => node.dataset.tooltip)
|
|
.map(node => {
|
|
const hint = getComputedStyle(node, '::after');
|
|
const host = node.getBoundingClientRect();
|
|
const hostStyle = getComputedStyle(node);
|
|
const width = parseFloat(hint.width) + (hint.boxSizing === 'border-box' ? 0
|
|
: parseFloat(hint.paddingLeft) + parseFloat(hint.paddingRight)
|
|
+ parseFloat(hint.borderLeftWidth) + parseFloat(hint.borderRightWidth));
|
|
const right = hint.right === 'auto'
|
|
? host.left + parseFloat(hostStyle.borderLeftWidth) + parseFloat(hint.left) + width
|
|
: host.right - parseFloat(hostStyle.borderRightWidth) - parseFloat(hint.right);
|
|
return right <= bounds.right + 1 && right - width >= bounds.left - 1;
|
|
});
|
|
const panel = document.querySelector('.model-dialog');
|
|
const luminance = color => {
|
|
const rgb = color.match(/[0-9.]+/g).slice(0, 3).map(Number)
|
|
.map(value => value / 255)
|
|
.map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4);
|
|
return rgb[0] * .2126 + rgb[1] * .7152 + rgb[2] * .0722;
|
|
};
|
|
const contrast = (first, second) => {
|
|
const a = luminance(first), b = luminance(second);
|
|
return (Math.max(a, b) + .05) / (Math.min(a, b) + .05);
|
|
};
|
|
const background = getComputedStyle(panel).backgroundColor;
|
|
const textContrast = [...panel.querySelectorAll('h2, p')]
|
|
.map(node => contrast(getComputedStyle(node).color, background));
|
|
const buttonContrast = [...panel.querySelectorAll('button')]
|
|
.map(node => { const style = getComputedStyle(node);
|
|
return contrast(style.color, style.backgroundColor); });
|
|
rows.push({theme: theme.id, language: language.id,
|
|
contrast: Math.min(...textContrast, ...buttonContrast),
|
|
client: aside.clientWidth, scroll: aside.scrollWidth,
|
|
outside, hintsInside: hints.every(Boolean)});
|
|
}
|
|
}
|
|
return rows;
|
|
})().then(rows => { window.sidebarProbe = {rows}; })
|
|
.catch(error => { window.sidebarProbe = {error: String(error)}; });""")
|
|
deadline = time.monotonic() + 40
|
|
probe = None
|
|
while time.monotonic() < deadline:
|
|
probe = window.evaluate_js('window.sidebarProbe')
|
|
if probe:
|
|
break
|
|
time.sleep(.1)
|
|
assert probe and 'rows' in probe, probe
|
|
results = probe['rows']
|
|
assert isinstance(results, list) and results, results
|
|
for row in results:
|
|
assert row['scroll'] <= row['client'] + 1, row
|
|
assert not row['outside'] and row['hintsInside'], row
|
|
assert row['contrast'] >= 4.5, row
|
|
print('THEME PASS', len(results), 'theme/language combinations; minimum model-panel contrast',
|
|
round(min(row['contrast'] for row in results), 2), flush=True)
|
|
|
|
|
|
def exercise(window, api):
|
|
"""Read real Alpine state and execute actions through JavaScript API promises."""
|
|
try:
|
|
assert window.events.loaded.wait(60), "Desktop page did not finish loading"
|
|
deadline = time.monotonic() + 20
|
|
state = None
|
|
while time.monotonic() < deadline:
|
|
state = window.evaluate_js("""JSON.stringify({
|
|
ready: !!window.appInstance && !window.appInstance.isLoading,
|
|
api: !!window.pywebview?.api,
|
|
version: window.appInstance?.appVersion,
|
|
translations: Object.keys(window.appInstance?.t || {}).length,
|
|
ram: window.appInstance?.systemInfo?.ram,
|
|
models: window.appInstance?.models?.status,
|
|
background: getComputedStyle(document.body).backgroundColor,
|
|
origin: location.protocol,
|
|
resources: performance.getEntriesByType('resource').map(x => x.name)
|
|
})""")
|
|
state = json.loads(state) if isinstance(state, str) else state
|
|
if state and state.get('ready') and state.get('translations') and state.get('ram', 0) > 0:
|
|
break
|
|
time.sleep(.2)
|
|
assert state and state['ready'] and state['api'], state
|
|
assert state['origin'] == 'http:', state
|
|
assert state['translations'] >= 10 and state['version'] != '0.0', state
|
|
assert all(url.startswith(('http://127.0.0.1:', 'http://localhost:')) for url in state['resources']), state
|
|
window.evaluate_js("document.querySelector('#models-button').click()")
|
|
assert window.evaluate_js('window.appInstance.modelsOpen')
|
|
window.evaluate_js("document.querySelector('#close-models').click()")
|
|
assert not window.evaluate_js('window.appInstance.modelsOpen')
|
|
window.evaluate_js("window.savedProbe = null; window.pywebview.api.save_config({theme:'korpo',lang:'en'}).then(() => window.pywebview.api.get_config()).then(x => window.savedProbe = x)")
|
|
deadline = time.monotonic() + 10
|
|
while time.monotonic() < deadline:
|
|
if window.evaluate_js("window.savedProbe?.theme") == 'korpo':
|
|
break
|
|
time.sleep(.1)
|
|
assert window.evaluate_js("window.savedProbe?.lang") == 'en'
|
|
check_sidebar_layout(window)
|
|
print('DESKTOP PASS', json.dumps(state), flush=True)
|
|
except BaseException as exc:
|
|
failures.append(repr(exc))
|
|
finally:
|
|
window.destroy()
|
|
|
|
|
|
def watchdog():
|
|
"""Bound native window startup and shutdown as well as the page checks."""
|
|
if not finished.wait(90):
|
|
print('Desktop check timed out', flush=True)
|
|
try:
|
|
check_config.cleanup()
|
|
finally:
|
|
os._exit(1)
|
|
|
|
|
|
threading.Thread(target=watchdog, daemon=True).start()
|
|
# WebKit can defer network page loading for an entirely hidden native window.
|
|
# Exercise its normal visible startup on the macOS CI desktop.
|
|
remwmgui.main(exercise, hidden=sys.platform != "darwin")
|
|
finished.set()
|
|
check_config.cleanup()
|
|
if failures:
|
|
raise SystemExit('; '.join(failures))
|