mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
Integrate lab fff55a78 into PR #40 (cc25d5ba). Lab's modular email backend/frontend, modular settings, split stylesheets (static/style.css stays deleted), procfs compatibility, and request-scoped TurnContract authority win; PR #40's routing classifiers, editor/email/task features, and style.css changes are ported into lab's module and stylesheet homes. Integration fixes: - settings/api.js imports ui.js under its canonical versioned URL - browser observations keep legacy CAPTCHA/access-block evidence - artifact turns do not re-trigger broad-web research recovery - env reference documents PR test-tool variables; page regenerated PR #40 defects surfaced by lab gates and fixed here: - web_fetch generic schema drops top-level anyOf (OpenAI contract); the compact preview contract still requires url or urls - get_weather registered as a brokered network read - new lazy editor modules precached for offline use - SearXNG pin mirrored into GPU standalone compose files - image model picker again skips offline endpoints Tests updated where PR #40 changed behaviour on purpose, and PR tests moved onto lab's document_source helpers.
74 lines
4.8 KiB
JavaScript
74 lines
4.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { chromium } from 'playwright';
|
|
import { appCss } from './helpers/stylesheets.mjs';
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const page = await browser.newPage();
|
|
await page.route('http://editor.test/**', async route => {
|
|
const path = new URL(route.request().url()).pathname;
|
|
if (path === '/') return route.fulfill({ contentType: 'text/html', body: '<button id="fx" style="position:fixed;bottom:4px;right:4px">fx</button>' });
|
|
if (!path.endsWith('.js')) return route.fulfill({ status: 404, body: '' });
|
|
return route.fulfill({ contentType: 'text/javascript', body: await readFile(new URL('../static/js/editor' + path, import.meta.url), 'utf8') });
|
|
});
|
|
await page.goto('http://editor.test/');
|
|
const results = await page.evaluate(async () => {
|
|
const { LAYER_STYLES } = await import('/layer-styles.js');
|
|
const { normalizeEffect, renderEffects, renderEffectsAsync, effectsWithPreview } = await import('/effects.js');
|
|
const source = document.createElement('canvas'); source.width = source.height = 64;
|
|
const ctx = source.getContext('2d'); ctx.fillStyle = '#668899'; ctx.fillRect(16, 16, 32, 32);
|
|
const original = ctx.getImageData(0, 0, 64, 64).data;
|
|
const shadow = normalizeEffect({ id: 'shadow', type: 'drop-shadow', params: { color: '#ff0000', opacity: 1, blur: 0, x: 8, y: 8 } });
|
|
const shadowed = renderEffects(source, [shadow]);
|
|
const pixel = (canvas, x, y) => [...canvas.getContext('2d').getImageData(x, y, 1, 1).data];
|
|
if (String(pixel(shadowed, 30, 30)) !== String(pixel(source, 30, 30))) throw Error('shadow covers source');
|
|
if (String(pixel(shadowed, 50, 50)) !== '255,0,0,255') throw Error('shadow offset or color');
|
|
const workerShadow = await renderEffectsAsync(source, [shadow]);
|
|
if (String(pixel(workerShadow, 50, 50)) !== String(pixel(shadowed, 50, 50))) throw Error('shadow worker mismatch');
|
|
const edited = { ...shadow, params: { ...shadow.params, x: 12 } };
|
|
const preview = effectsWithPreview({ effects: [shadow], _effectPreview: edited });
|
|
if (preview.length !== 1 || preview[0] !== edited) throw Error('duplicate preview');
|
|
const result = [];
|
|
for (const type of Object.keys(LAYER_STYLES)) {
|
|
const effect = normalizeEffect({ type });
|
|
if (normalizeEffect(JSON.parse(JSON.stringify(effect))).type !== type) throw Error('restore ' + type);
|
|
const sync = renderEffects(source, [effect]).getContext('2d').getImageData(0, 0, 64, 64).data;
|
|
if (!sync.some((v, i) => v !== original[i])) throw Error('no effect ' + type);
|
|
const asyncCanvas = await renderEffectsAsync(source, [effect]);
|
|
const asyncPixels = asyncCanvas.getContext('2d').getImageData(0, 0, 64, 64).data;
|
|
const difference = sync.reduce((max, v, i) => Math.max(max, Math.abs(v - asyncPixels[i])), 0);
|
|
if (difference > 2) throw Error('worker mismatch ' + type + ': ' + difference);
|
|
const hidden = renderEffects(source, [{ ...effect, visible: false }]).getContext('2d').getImageData(0, 0, 64, 64).data;
|
|
if (hidden.some((v, i) => v !== original[i])) throw Error('hidden ' + type);
|
|
if (ctx.getImageData(0, 0, 64, 64).data.some((v, i) => v !== original[i])) throw Error('mutated ' + type);
|
|
result.push(type);
|
|
}
|
|
return result;
|
|
});
|
|
assert.equal(results.length, 7);
|
|
await page.addStyleTag({ content: await appCss() });
|
|
await page.evaluate(async () => {
|
|
const { openLayerStyleMenu } = await import('/layer-style-menu.js');
|
|
document.querySelector('#fx').onclick = event => { event.stopPropagation(); openLayerStyleMenu(event.currentTarget, () => {}); };
|
|
});
|
|
for (const viewport of [{ width: 1280, height: 720 }, { width: 375, height: 420 }]) {
|
|
await page.setViewportSize(viewport);
|
|
await page.locator('#fx').click();
|
|
const menu = page.locator('.ge-layer-style-menu');
|
|
assert.equal(await menu.locator('button').count(), 10);
|
|
const bounds = await menu.boundingBox();
|
|
assert.equal(await menu.evaluate(el => getComputedStyle(el).opacity), '1');
|
|
assert.ok(bounds.x >= 0 && bounds.y >= 0 && bounds.x + bounds.width <= viewport.width && bounds.y + bounds.height <= viewport.height);
|
|
await page.screenshot({ path: `/tmp/editor-layer-styles-${viewport.width}.png` });
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(30);
|
|
assert.equal(await menu.count(), 0);
|
|
}
|
|
await page.locator('#fx').click();
|
|
await page.locator('#fx').click();
|
|
await page.waitForTimeout(30);
|
|
assert.equal(await page.locator('.ge-layer-style-menu').count(), 0, 'repeat click closes menu');
|
|
console.log('7 styles: render, worker parity, restoration, visibility and source preservation passed; 10-item menu fits desktop/mobile and closes on Escape.');
|
|
} finally { await browser.close(); }
|