Comparar commits
4 Commits
google-tab
...
v1.14.1
| Autor | SHA1 | Fecha | |
|---|---|---|---|
|
|
261acc0201 | ||
|
|
1bdc7d0bba | ||
|
|
013d1e7759 | ||
|
|
a10805c51b |
17
.github/workflows/build.yml
vendido
17
.github/workflows/build.yml
vendido
@@ -51,6 +51,9 @@ jobs:
|
||||
- name: Generar iconos (desde app-icon.png)
|
||||
run: npm run icons
|
||||
|
||||
- name: Probar el cliente
|
||||
run: npm test
|
||||
|
||||
- name: Construir, firmar y publicar
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -63,6 +66,18 @@ jobs:
|
||||
tagName: ${{ github.ref_type == 'tag' && github.ref_name || 'v__VERSION__' }}
|
||||
releaseName: "Labstream OS Desktop __VERSION__"
|
||||
releaseBody: "Instaladores de escritorio de Labstream OS.\n\n- Windows: el archivo `.exe` (NSIS).\n- macOS: el archivo `.dmg` universal (Intel + Apple Silicon).\n\nActualización automática activada: las apps ya instaladas se actualizan solas al publicar una versión nueva."
|
||||
releaseDraft: false
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
activar:
|
||||
needs: publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Publicar solo cuando ambos instaladores estan completos
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
gh release edit "v$VERSION" --draft=false --latest
|
||||
|
||||
3
.github/workflows/check.yml
vendido
3
.github/workflows/check.yml
vendido
@@ -44,6 +44,9 @@ jobs:
|
||||
- name: Instalar dependencias
|
||||
run: npm install
|
||||
|
||||
- name: Probar el script del cliente
|
||||
run: npm test
|
||||
|
||||
# build.rs de Tauri valida que existan los iconos que declara tauri.conf.json.
|
||||
- name: Generar iconos (desde app-icon.png)
|
||||
run: npm run icons
|
||||
|
||||
22
README.md
22
README.md
@@ -84,6 +84,28 @@ pestañas son webviews hijos `tab-*`. Shell ↔ Rust se hablan por eventos `ls-*
|
||||
|
||||
El servidor objetivo se configura en `src-tauri/src/lib.rs` → constante `SERVER_URL`.
|
||||
|
||||
### Portadas autenticadas (1.14.1)
|
||||
|
||||
Las imagenes conservan su URL HTTPS y se cargan con la sesion del webview, igual
|
||||
que en el navegador. Ya no se reescriben a `lsthumb://`: el descargador Rust no
|
||||
comparte las cookies y fallaba con miniaturas sin token o con token vencido.
|
||||
Tampoco se modifica el DOM de las imagenes durante la hidratacion.
|
||||
|
||||
Se usa la cache HTTP con los ETag y Cache-Control del servidor. Una respuesta
|
||||
304 reutiliza los bytes descargados; no implica fabricar otro fotograma. El
|
||||
cajon antiguo no se borra, pero no se usa para las nuevas peticiones de imagenes.
|
||||
No cambia el almacenamiento de originales, la cache HLS de LabTem ni los datos offline.
|
||||
|
||||
Google Docs, Sheets, Drive y el inicio de sesion de Google se abren en el navegador
|
||||
del sistema. Google no admite OAuth en webviews controlados por una app; ademas,
|
||||
WKWebView no debe anunciarse como Chrome. Se conserva el agente de usuario nativo
|
||||
y no se borran cookies ni sesiones. Las hojas conectadas siguen disponibles como
|
||||
tabla dentro del area contable. Al reiniciar se restauran solo pestanas de Labstream.
|
||||
|
||||
Pruebas: `npm test`; con Playwright disponible en Node,
|
||||
`node tests/images.browser.cjs` verifica sesion, 304, carga dinamica y lienzo.
|
||||
La prueba de navegador usa un servidor local y una cookie ficticia, sin datos reales.
|
||||
|
||||
Salidas:
|
||||
|
||||
- **Windows** → instalador `.exe` (NSIS).
|
||||
|
||||
4
package-lock.json
generado
4
package-lock.json
generado
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "labstream-desktop",
|
||||
"version": "1.4.0",
|
||||
"version": "1.14.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "labstream-desktop",
|
||||
"version": "1.4.0",
|
||||
"version": "1.14.1",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "labstream-desktop",
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.1",
|
||||
"private": true,
|
||||
"description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.",
|
||||
"scripts": {
|
||||
"test": "node --test tests/images.test.cjs",
|
||||
"icons": "tauri icon app-icon.png",
|
||||
"dev": "npm run icons && tauri dev",
|
||||
"build": "npm run icons && tauri build",
|
||||
|
||||
2
src-tauri/Cargo.lock
generado
2
src-tauri/Cargo.lock
generado
@@ -2366,7 +2366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "labstream-desktop"
|
||||
version = "1.13.0"
|
||||
version = "1.14.1"
|
||||
dependencies = [
|
||||
"active-win-pos-rs",
|
||||
"device_query",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "labstream-desktop"
|
||||
version = "1.13.0"
|
||||
version = "1.14.1"
|
||||
description = "Cliente de escritorio de Labstream OS"
|
||||
authors = ["Labstream SAS"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -54,11 +54,7 @@ use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
const SERVER_URL: &str = "https://os.labstreamsas.com";
|
||||
|
||||
// Los documentos del contador viven en Google (pestaña Contable). Se abren como PESTANAS
|
||||
// de esta app, no en el navegador externo: asi el WebView guarda la sesion de Google del
|
||||
// usuario (cookies persistentes) y no hay que iniciar sesion cada vez. La lista es cerrada
|
||||
// a proposito: solo hosts propios de Google (docs/drive/sheets y accounts para el login).
|
||||
// Todo lo demas sigue yendo al navegador del sistema.
|
||||
// Google requiere el navegador del sistema para autenticar y editar con compatibilidad completa.
|
||||
const GOOGLE_HOSTS: &[&str] = &[
|
||||
"docs.google.com",
|
||||
"drive.google.com",
|
||||
@@ -73,13 +69,23 @@ fn is_google_host(host: &str) -> bool {
|
||||
}
|
||||
|
||||
fn es_pestana_interna(target: &Url, server: &Url) -> bool {
|
||||
if target.origin() == server.origin() {
|
||||
return true;
|
||||
target.origin() == server.origin()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod navigation_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_labstream_is_an_embedded_tab() {
|
||||
let server = Url::parse(SERVER_URL).unwrap();
|
||||
assert!(es_pestana_interna(&server.join("/revisiones/1").unwrap(), &server));
|
||||
for url in ["https://docs.google.com/document/d/1/edit", "https://accounts.google.com", "https://os.labstreamsas.com.evil.example", "http://os.labstreamsas.com"] {
|
||||
assert!(!es_pestana_interna(&Url::parse(url).unwrap(), &server));
|
||||
}
|
||||
assert!(is_google_host("docs.google.com"));
|
||||
assert!(!is_google_host("docs.google.com.evil.example"));
|
||||
}
|
||||
if target.scheme() != "https" {
|
||||
return false;
|
||||
}
|
||||
target.host_str().map(is_google_host).unwrap_or(false)
|
||||
}
|
||||
const TAB_BAR_H: f64 = 40.0; // alto lógico de la barra de pestañas (chrome)
|
||||
const CHROME: &str = "chrome";
|
||||
@@ -233,12 +239,7 @@ const INIT_JS_TPL: &str = r#"
|
||||
// descargar fallaban solo en la app de Windows, y no en Mac ni en el navegador.
|
||||
// En macOS estas dos líneas son un no-op: allí el script nunca ha corrido en subframes.
|
||||
try { if (window.top !== window.self) return; } catch (e) { return; }
|
||||
// Hosts de Google que la app abre como pestana propia (pestana Contable). En esas
|
||||
// paginas solo reportamos titulo a la barra; NO tocamos clics ni window.open para no
|
||||
// romper el editor de Google.
|
||||
var GOOGLE_RE = /^(?:.*\.)?(?:docs|drive|sheets|accounts|myaccount)\.google\.com$/i;
|
||||
var esGoogle = GOOGLE_RE.test(location.hostname);
|
||||
if (location.origin !== ORIGIN && !esGoogle) return;
|
||||
if (location.origin !== ORIGIN) return;
|
||||
|
||||
if (window.__lsShell) return;
|
||||
window.__lsShell = true;
|
||||
@@ -249,8 +250,7 @@ const INIT_JS_TPL: &str = r#"
|
||||
function abs(h) { try { return new URL(h, location.href); } catch (e) { return null; } }
|
||||
function isHttp(u) { return !!u && (u.protocol === 'http:' || u.protocol === 'https:'); }
|
||||
function isApp(u) { return isHttp(u) && u.origin === ORIGIN; }
|
||||
function isGoogleUrl(u) { return isHttp(u) && GOOGLE_RE.test(u.hostname); }
|
||||
function isInterno(u) { return isApp(u) || isGoogleUrl(u); }
|
||||
function isInterno(u) { return isApp(u); }
|
||||
function isExt(u) { return isHttp(u) && !isInterno(u); }
|
||||
function emit(name, payload) { try { T.event.emit(name, payload); } catch (e) {} }
|
||||
function openExternal(url) {
|
||||
@@ -259,73 +259,10 @@ const INIT_JS_TPL: &str = r#"
|
||||
}
|
||||
function newTab(url) { emit('ls-new-tab', { url: url }); }
|
||||
|
||||
// ── Miniaturas por el CAJÓN de la app (lsthumb://) ──
|
||||
// Dentro de la app de escritorio, las miniaturas de la cuadrícula de fotos se piden por el
|
||||
// esquema lsthumb:// en vez de directo al servidor. Así las sirve el cajón local (Rust): del
|
||||
// disco si ya están —sobreviven a cerrar la app, que es lo que se pedía— y del NAS solo la
|
||||
// primera vez. Fuera de la app (navegador normal) este script no corre y todo sigue igual.
|
||||
//
|
||||
// Se reescribe SOLO la miniatura de 480 px (thumb=1): es la que se repite 500 veces por set y
|
||||
// ahoga la cuadrícula. El visor grande (thumb=xl) y la anotación sobre lienzo NO se tocan —
|
||||
// cargar la imagen del lienzo por otro esquema lo «ensuciaría» y rompería «guardar el dibujo».
|
||||
try {
|
||||
function porCajon(src) {
|
||||
if (!src || src.lastIndexOf('lsthumb:', 0) === 0) return src; // ya reescrita
|
||||
var u = abs(src);
|
||||
if (!u || u.origin !== ORIGIN) return src; // solo lo del propio servidor
|
||||
if (!/\/api\/files-asset\//.test(u.pathname) || u.searchParams.get('thumb') !== '1') return src;
|
||||
return 'lsthumb://c/?u=' + encodeURIComponent(u.href);
|
||||
}
|
||||
var proto = HTMLImageElement.prototype;
|
||||
// 1) La propiedad .src (lo que usa React al asignar la imagen).
|
||||
var d = Object.getOwnPropertyDescriptor(proto, 'src');
|
||||
if (d && d.get && d.set) {
|
||||
Object.defineProperty(proto, 'src', {
|
||||
configurable: true, enumerable: d.enumerable,
|
||||
get: function () { return d.get.call(this); },
|
||||
set: function (v) { d.set.call(this, porCajon(String(v))); },
|
||||
});
|
||||
}
|
||||
// 2) setAttribute('src', …), que no pasa por el setter de la propiedad. Se parchea SOLO en
|
||||
// <img> (prototipo de HTMLImageElement), no en todos los elementos.
|
||||
var setA = proto.setAttribute;
|
||||
proto.setAttribute = function (name, value) {
|
||||
if (arguments.length >= 2 && String(name).toLowerCase() === 'src') {
|
||||
return setA.call(this, name, porCajon(String(value)));
|
||||
}
|
||||
return setA.apply(this, arguments);
|
||||
};
|
||||
// 3) Las que ya vienen en el HTML del servidor (SSR) o llegan luego. porCajon es idempotente
|
||||
// (una ya reescrita se devuelve igual), así que barrer no entra en bucle con el observer.
|
||||
function barrer(raiz) {
|
||||
if (!raiz || !raiz.querySelectorAll) return;
|
||||
var imgs = raiz.querySelectorAll('img[src]');
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var cur = imgs[i].getAttribute('src');
|
||||
var nue = porCajon(cur);
|
||||
if (nue !== cur) imgs[i].setAttribute('src', nue);
|
||||
}
|
||||
}
|
||||
if (window.MutationObserver) {
|
||||
new MutationObserver(function (muts) {
|
||||
for (var i = 0; i < muts.length; i++) {
|
||||
var m = muts[i];
|
||||
if (m.type === 'attributes' && m.target && m.target.tagName === 'IMG') {
|
||||
var cur = m.target.getAttribute('src'), nue = porCajon(cur);
|
||||
if (nue !== cur) m.target.setAttribute('src', nue);
|
||||
}
|
||||
for (var j = 0; j < m.addedNodes.length; j++) {
|
||||
var n = m.addedNodes[j];
|
||||
if (!n || n.nodeType !== 1) continue;
|
||||
if (n.tagName === 'IMG') { var c = n.getAttribute('src'), x = porCajon(c); if (x !== c) n.setAttribute('src', x); }
|
||||
else barrer(n);
|
||||
}
|
||||
}
|
||||
}).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] });
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { barrer(document); });
|
||||
else barrer(document);
|
||||
} catch (e) { /* si algo falla, las imágenes cargan directo del servidor como siempre */ }
|
||||
// Las imagenes conservan su URL HTTPS y la sesion del webview. El descargador
|
||||
// lsthumb no comparte cookies: falla con miniaturas sin token y omite la
|
||||
// revalidacion de permisos al leer del disco. La cache HTTP respeta los ETag
|
||||
// y Cache-Control del servidor sin modificar src, srcset ni la hidratacion.
|
||||
|
||||
// ── ¿La página se VE oscura? ──
|
||||
// Señales, de la más directa a la más física: la clase `dark` (next-themes) y, si no
|
||||
@@ -378,7 +315,6 @@ const INIT_JS_TPL: &str = r#"
|
||||
history[k] = function () { var r = o.apply(this, arguments); setTimeout(report, 0); return r; };
|
||||
});
|
||||
addEventListener('popstate', function () { setTimeout(report, 0); });
|
||||
if (esGoogle) return; // en Google solo miramos el titulo
|
||||
// Pulsar la luna cambia clases/estilos sin navegar: se vigilan <html> y <body> (cuando
|
||||
// existan — a document-start aún no hay), y tras la carga se re-reporta un par de veces
|
||||
// por si el tema aterriza tarde con la hidratación.
|
||||
@@ -401,9 +337,6 @@ const INIT_JS_TPL: &str = r#"
|
||||
if (!u) return;
|
||||
if (isExt(u)) { e.preventDefault(); openExternal(u.href); return; }
|
||||
var mod = e.metaKey || e.ctrlKey;
|
||||
// Enlace a Google (docs, drive, sheets, accounts): SIEMPRE como pestana propia
|
||||
// aunque no lleve target=_blank, para que la sesion viva dentro de la app.
|
||||
if (isGoogleUrl(u)) { e.preventDefault(); newTab(u.href); return; }
|
||||
if (isApp(u) && (mod || a.target === '_blank')) { e.preventDefault(); newTab(u.href); }
|
||||
}, true);
|
||||
|
||||
@@ -602,8 +535,16 @@ fn create_tab<R: Runtime>(app: &AppHandle<R>, url: Option<String>, activate: boo
|
||||
|
||||
let init = INIT_JS_TPL.replace("__LABEL__", &label).replace("__ORIGIN__", SERVER_URL);
|
||||
let app_zoom = app.clone();
|
||||
let app_navigation = app.clone();
|
||||
let builder = WebviewBuilder::new(&label, WebviewUrl::External(parsed))
|
||||
.initialization_script(&init)
|
||||
.on_navigation(move |url| {
|
||||
if url.scheme() == "https" && url.host_str().map(is_google_host).unwrap_or(false) {
|
||||
let _ = app_navigation.opener().open_url(url.as_str(), None::<&str>);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
// Deja pasar las descargas (entregables, exportaciones) Y avisa al terminar.
|
||||
// El aviso no es un adorno: en WebView2 la interfaz de descargas queda SIEMPRE
|
||||
// apagada. wry trae un manejador de descargas por defecto (wry-0.55.1/src/lib.rs:830),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Labstream OS",
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.1",
|
||||
"identifier": "co.labstream.os",
|
||||
"build": {
|
||||
"frontendDist": "../dist"
|
||||
|
||||
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