Release 2.20.6: saved-image slide previews
This commit is contained in:
1 parent
205ce42337
commit
24df245ea1
20 files changed
+318
-75
No files matched your search
@@ -35,6 +35,111 @@ function loadEditorTimeDateModule() {
|
||||
return modules.get('time-date');
|
||||
}
|
||||
|
||||
test('list preview loads a saved image with legacy fallback and clears it on close', () => {
|
||||
const source = fs.readFileSync(require.resolve('../src/web/views/signage/slides/list.hbs'), 'utf8');
|
||||
assert.doesNotMatch(source, /<iframe|\/api\/internal\/slide-thumbnails/);
|
||||
assert.match(source, /{{> slide-preview-modal}}/);
|
||||
const modalSource = fs.readFileSync(require.resolve('../src/web/views/shared/slide-preview-modal.hbs'), 'utf8');
|
||||
const css = fs.readFileSync(require.resolve('../src/web/public/css/theme-custom.css'), 'utf8');
|
||||
assert.match(css, /\.slide-list-preview-frame\[hidden\]\s*\{\s*display: none;/);
|
||||
const handlers = {};
|
||||
const image = {
|
||||
addEventListener(name, handler) { handlers['image:' + name] = handler; },
|
||||
getAttribute(name) { return this[name]; },
|
||||
removeAttribute(name) { delete this[name]; }
|
||||
};
|
||||
const empty = {};
|
||||
const title = {};
|
||||
const modal = {
|
||||
querySelector(selector) {
|
||||
return selector === '[data-slide-preview-image]' ? image : selector === '[data-slide-preview-empty]' ? empty : title;
|
||||
},
|
||||
addEventListener(name, handler) { handlers[name] = handler; },
|
||||
style: { setProperty() {} }
|
||||
};
|
||||
vm.runInNewContext(modalSource.match(/<script>([\s\S]*?)<\/script>/)[1], {
|
||||
document: { getElementById() { return modal; } },
|
||||
window: { innerWidth: 1440, innerHeight: 900, addEventListener() {} }
|
||||
});
|
||||
function open(url) {
|
||||
handlers['show.bs.modal']({ relatedTarget: { getAttribute(name) {
|
||||
return { 'data-slide-preview-url': url, 'data-slide-preview-title': 'Saved slide', 'data-canvas-width': '1080', 'data-canvas-height': '1920' }[name];
|
||||
} } });
|
||||
}
|
||||
open('/media/thumbnails/slides/slide-7.png');
|
||||
assert.equal(image.src, '/media/thumbnails/slides/slide-7.full.png');
|
||||
assert.equal(image.hidden, false);
|
||||
assert.equal(title.textContent, 'Slide preview: Saved slide');
|
||||
handlers['image:error']();
|
||||
assert.equal(image.src, '/media/thumbnails/slides/slide-7.png');
|
||||
handlers['image:error']();
|
||||
assert.equal(image.hidden, true);
|
||||
assert.equal(empty.hidden, false);
|
||||
open('/media/thumbnails/slides/slide-8.png');
|
||||
handlers['hidden.bs.modal']();
|
||||
assert.equal(image.src, undefined);
|
||||
assert.equal(image.hidden, true);
|
||||
open('');
|
||||
assert.equal(image.src, undefined);
|
||||
assert.equal(empty.hidden, false);
|
||||
});
|
||||
|
||||
test('thumbnail capture retains a full-size preview alongside the small thumbnail', async () => {
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const sharp = require('sharp');
|
||||
const modulePath = require.resolve('../src/web/lib/media/slide-thumbnails');
|
||||
const localRequire = require('node:module').createRequire(modulePath);
|
||||
const mediaDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-preview-'));
|
||||
const page = {
|
||||
async setViewport() {},
|
||||
async setExtraHTTPHeaders() {},
|
||||
async goto() { return { status() { return 200; } }; },
|
||||
async waitForFunction() {},
|
||||
async evaluate() {},
|
||||
async $() { return { async boundingBox() { return { x: 0, y: 0, width: 1080, height: 1920 }; } }; },
|
||||
async screenshot(options) {
|
||||
await sharp({ create: { width: options.clip.width, height: options.clip.height, channels: 3, background: '#123456' } }).png().toFile(options.path);
|
||||
},
|
||||
async close() {}
|
||||
};
|
||||
const sandbox = {
|
||||
module: { exports: {} },
|
||||
process: { env: {} },
|
||||
setTimeout(callback) { callback(); },
|
||||
require(name) {
|
||||
if (name === 'fs') return { ...fs, existsSync(candidate) { return candidate === '/usr/bin/chromium' || fs.existsSync(candidate); } };
|
||||
if (name === 'puppeteer-core') return { async launch() { return { async newPage() { return page; }, async close() {} }; } };
|
||||
return localRequire(name);
|
||||
}
|
||||
};
|
||||
sandbox.require.resolve = localRequire.resolve;
|
||||
vm.runInNewContext(fs.readFileSync(modulePath, 'utf8'), sandbox);
|
||||
const queries = [];
|
||||
try {
|
||||
const options = {
|
||||
mediaDir,
|
||||
slideId: 7,
|
||||
baseUrl: 'http://localhost:8080',
|
||||
pool: { async query(sql, params) { queries.push({ sql, params }); } },
|
||||
common: { async fetchSlideById() { return { id: 7, template: { canvas_size_width: 1080, canvas_size_height: 1920 } }; } }
|
||||
};
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const result = await sandbox.module.exports.captureSlideThumbnail(options);
|
||||
const large = await sharp(result.fullSizePath).metadata();
|
||||
const small = await sharp(result.filePath).metadata();
|
||||
assert.equal(large.width, 1080);
|
||||
assert.equal(large.height, 1920);
|
||||
assert.ok(small.width <= 480 && small.height <= 270);
|
||||
assert.equal(result.thumbnailPath, '/media/thumbnails/slides/slide-7.png');
|
||||
assert.equal(fs.existsSync(result.fullSizePath.replace('.full.png', '.full.tmp.png')), false);
|
||||
}
|
||||
assert.equal(queries.length, 2);
|
||||
} finally {
|
||||
await fs.promises.rm(mediaDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('preview sanitizers preserve entity-quoted font families without double escaping', () => {
|
||||
const entities = { apos: "'", quot: '"', amp: '&', lt: '<', gt: '>' };
|
||||
const sandbox = {
|
||||
|
||||
Reference in new issue
Block a user