fix(desktop): authenticated previews and reliable Google document opening
Este commit está contenido en:
84
tests/images.browser.cjs
Archivo normal
84
tests/images.browser.cjs
Archivo normal
@@ -0,0 +1,84 @@
|
||||
// Run with Playwright available: node tests/images.browser.cjs
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const http = require('node:http');
|
||||
const { chromium, webkit } = require('playwright');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const source = fs.readFileSync(path.join(root, 'src-tauri/src/lib.rs'), 'utf8');
|
||||
const template = source.match(/const INIT_JS_TPL: &str = r#"([\s\S]*?)"#;/)[1];
|
||||
const png = fs.readFileSync(path.join(root, 'app-icon.png'));
|
||||
const urls = [
|
||||
'/api/files-asset/session?thumb=1',
|
||||
'/api/files-asset/signed?thumb=1&t=expired.sig',
|
||||
'/api/files-asset/poster?poster=1',
|
||||
'/api/files-asset/large?thumb=xl',
|
||||
'/api/files-asset/original',
|
||||
];
|
||||
const counts = { authenticated: 0, revalidated: 0, unauthorized: 0 };
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url.startsWith('/api/files-asset/')) {
|
||||
if (req.headers.cookie !== 'test-session=ok') {
|
||||
counts.unauthorized++;
|
||||
res.writeHead(401, { 'Cache-Control': 'no-store' }).end();
|
||||
return;
|
||||
}
|
||||
counts.authenticated++;
|
||||
const headers = { 'Content-Type': 'image/png', 'Cache-Control': 'private, no-cache', ETag: '"cover-1"' };
|
||||
if (req.headers['if-none-match'] === '"cover-1"') {
|
||||
counts.revalidated++;
|
||||
res.writeHead(304, headers).end();
|
||||
} else res.writeHead(200, headers).end(png);
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-store' });
|
||||
res.end(`<html><title>Portadas: prueba local</title><body>
|
||||
${urls.map((url, i) => `<img id="image-${i}" width="120" height="120" src="${url}">`).join('')}
|
||||
<canvas width="16" height="16"></canvas></body></html>`);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
let browser;
|
||||
try {
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const origin = `http://127.0.0.1:${server.address().port}`;
|
||||
browser = await (process.env.TEST_BROWSER === 'webkit' ? webkit : chromium).launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
await context.addCookies([{ name: 'test-session', value: 'ok', url: origin, httpOnly: true }]);
|
||||
await context.addInitScript(`window.__TAURI__ = { event: { emit() {} } };`);
|
||||
await context.addInitScript(template.replaceAll('__ORIGIN__', origin).replaceAll('__LABEL__', 'tab-test'));
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await page.goto(origin);
|
||||
await page.waitForFunction(() => [...document.images].every((image) => image.complete && image.naturalWidth > 0));
|
||||
assert.deepEqual(await page.locator('img').evaluateAll((images) => images.map((image) => image.getAttribute('src'))), urls);
|
||||
// Cached images must still be readable by the annotation canvas.
|
||||
assert.equal(await page.evaluate(() => {
|
||||
const canvas = document.querySelector('canvas');
|
||||
canvas.getContext('2d').drawImage(document.images[0], 0, 0, 16, 16);
|
||||
return canvas.toDataURL().startsWith('data:image/png;');
|
||||
}), true);
|
||||
await page.reload();
|
||||
await page.waitForFunction(() => [...document.images].every((image) => image.complete && image.naturalWidth > 0));
|
||||
assert.ok(counts.revalidated >= urls.length, 'HTTP cache revalidation expected on reload');
|
||||
await page.evaluate(() => {
|
||||
const image = new Image();
|
||||
image.id = 'dynamic';
|
||||
image.setAttribute('src', '/api/files-asset/dynamic?thumb=1');
|
||||
document.body.appendChild(image);
|
||||
});
|
||||
await page.waitForFunction(() => document.getElementById('dynamic').naturalWidth > 0);
|
||||
// A native downloader without the webview session reproduces the 401.
|
||||
assert.equal((await fetch(origin + urls[0])).status, 401);
|
||||
await context.clearCookies();
|
||||
await page.reload();
|
||||
assert.equal(await page.locator('img').evaluateAll((images) => images.filter((image) => image.naturalWidth > 0).length), 0);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(JSON.stringify({ ok: true, covers: urls.length, dynamic: true, canvas: true, ...counts }));
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
})().catch((error) => { console.error(error); process.exitCode = 1; });
|
||||
102
tests/images.test.cjs
Archivo normal
102
tests/images.test.cjs
Archivo normal
@@ -0,0 +1,102 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname, '../src-tauri/src/lib.rs'), 'utf8');
|
||||
const template = source.match(/const INIT_JS_TPL: &str = r#"([\s\S]*?)"#;/)[1];
|
||||
const origin = 'https://os.labstreamsas.com';
|
||||
|
||||
function shell({ frame = false, host = origin, images = [] } = {}) {
|
||||
class Image {
|
||||
constructor(src = '') { this.attrs = { src }; }
|
||||
get src() { return this.attrs.src; }
|
||||
set src(value) { this.attrs.src = value; }
|
||||
getAttribute(name) { return this.attrs[name]; }
|
||||
setAttribute(name, value) { this.attrs[name] = value; }
|
||||
}
|
||||
const setter = Object.getOwnPropertyDescriptor(Image.prototype, 'src').set;
|
||||
const setAttribute = Image.prototype.setAttribute;
|
||||
const emitted = [];
|
||||
const opened = [];
|
||||
const listeners = {};
|
||||
const nodes = images.map((src) => new Image(src));
|
||||
const root = { classList: { contains: () => false } };
|
||||
const win = {
|
||||
__TAURI__: { event: { emit: (...args) => emitted.push(args) }, core: { invoke: (...args) => opened.push(args) } },
|
||||
open: () => 'original',
|
||||
};
|
||||
win.self = win;
|
||||
win.top = frame ? {} : win;
|
||||
const sandbox = {
|
||||
window: win, URL, HTMLImageElement: Image,
|
||||
location: new URL(host + '/proyectos/ejemplo'),
|
||||
document: {
|
||||
readyState: 'complete', title: 'Entregables', documentElement: root, body: root,
|
||||
querySelector: () => null, querySelectorAll: () => nodes,
|
||||
addEventListener: (name, callback) => { listeners[name] = callback; },
|
||||
},
|
||||
history: {}, navigator: { platform: 'MacIntel' },
|
||||
getComputedStyle: () => ({ backgroundColor: 'rgb(255, 255, 255)' }),
|
||||
addEventListener: (name, callback) => { listeners[name] = callback; },
|
||||
setTimeout: () => 0,
|
||||
};
|
||||
vm.runInNewContext(template.replaceAll('__ORIGIN__', origin).replaceAll('__LABEL__', 'tab-1'), sandbox);
|
||||
return { Image, nodes, setter, setAttribute, win, emitted, opened, listeners };
|
||||
}
|
||||
|
||||
test('does not replace native image setters or rewrite server-rendered covers', () => {
|
||||
const urls = ['/api/files-asset/cover?thumb=1', '/api/files-asset/cover?t=123.sig&thumb=1'];
|
||||
const state = shell({ images: urls });
|
||||
assert.equal(Object.getOwnPropertyDescriptor(state.Image.prototype, 'src').set, state.setter);
|
||||
assert.equal(state.Image.prototype.setAttribute, state.setAttribute);
|
||||
assert.deepEqual(state.nodes.map((image) => image.src), urls);
|
||||
});
|
||||
|
||||
for (const url of [
|
||||
'/api/files-asset/cover?thumb=1',
|
||||
'/api/files-asset/cover?t=expired.sig&thumb=1&v=abc',
|
||||
'/api/files-asset/cover?thumb=xl',
|
||||
'/api/files-asset/video?poster=1',
|
||||
'/api/files-asset/original',
|
||||
'https://other.example/image.webp',
|
||||
]) {
|
||||
test(`keeps authenticated browser transport for ${url}`, () => {
|
||||
const { Image } = shell();
|
||||
const image = new Image();
|
||||
image.src = url;
|
||||
assert.equal(image.src, url);
|
||||
image.setAttribute('src', url + '&r=1');
|
||||
assert.equal(image.src, url + '&r=1');
|
||||
});
|
||||
}
|
||||
|
||||
test('tab opening and title reports still work', () => {
|
||||
const { win, emitted } = shell();
|
||||
win.open('/revisiones/123', '_blank');
|
||||
assert.equal(emitted[0][0], 'ls-title');
|
||||
assert.equal(emitted[0][1].title, 'Entregables');
|
||||
assert.equal(emitted[1][0], 'ls-new-tab');
|
||||
assert.equal(emitted[1][1].url, origin + '/revisiones/123');
|
||||
});
|
||||
|
||||
test('does not interfere with embedded editors or unrelated origins', () => {
|
||||
for (const options of [{ frame: true }, { host: 'https://docs.example.com' }, { host: 'https://docs.google.com' }]) {
|
||||
const { win, emitted } = shell(options);
|
||||
assert.equal(win.__lsShell, undefined);
|
||||
assert.equal(win.open(), 'original');
|
||||
assert.equal(emitted.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('Google editors and login use the system browser, never an embedded tab', () => {
|
||||
for (const host of ['docs', 'sheets', 'drive', 'accounts']) {
|
||||
const { win, emitted, opened } = shell();
|
||||
const url = `https://${host}.google.com/example`;
|
||||
win.open(url, '_blank');
|
||||
assert.equal(emitted.filter(([name]) => name === 'ls-new-tab').length, 0);
|
||||
assert.equal(opened[0][0], 'plugin:opener|open_url');
|
||||
assert.equal(opened[0][1].url, url);
|
||||
}
|
||||
});
|
||||
Referencia en una nueva incidencia
Block a user