Release 2.21.0: non-destructive media and scoped player sync
This commit is contained in:
1 parent
24df245ea1
commit
7cff39dc76
35 files changed
+2009
-949
No files matched your search
@@ -462,7 +462,7 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
|
||||
]);
|
||||
});
|
||||
|
||||
function createSharedLayoutRouteHarness(failUpdate, allowSlideUpdate) {
|
||||
function createSharedLayoutRouteHarness(failUpdate, allowSlideUpdate, imageOptions) {
|
||||
const registerContentRoutes = require('../src/web/routes/admin/content');
|
||||
const handlers = {};
|
||||
const registrations = [];
|
||||
@@ -475,6 +475,13 @@ function createSharedLayoutRouteHarness(failUpdate, allowSlideUpdate) {
|
||||
id: 11, region_key: 'hero', region_type: 'text', x: 10, y: 20, width: 300, height: 100,
|
||||
z_index: 1, lock_ratio: null, animation_json: '{"intro":{"preset":"fadeIn"}}'
|
||||
}] };
|
||||
if (imageOptions) {
|
||||
template.regions[0].region_type = 'image';
|
||||
template.regions[0].width = 300;
|
||||
template.regions[0].height = 200;
|
||||
if (imageOptions.defaultContent) template.regions[0].default_content = imageOptions.defaultContent;
|
||||
savedContent.hero = imageOptions.content;
|
||||
}
|
||||
const connection = {
|
||||
async beginTransaction() { calls.push(['begin']); },
|
||||
async commit() { calls.push(['commit']); },
|
||||
@@ -483,6 +490,10 @@ function createSharedLayoutRouteHarness(failUpdate, allowSlideUpdate) {
|
||||
async query(sql, params) {
|
||||
calls.push([sql, params]);
|
||||
if (sql.startsWith('SELECT id, template_id, content_json')) return [[{ id: 42, template_id: 7, content_json: JSON.stringify(savedContent) }]];
|
||||
if (sql.startsWith('SELECT id, content_json FROM c_slides')) return [[{ id: 42, content_json: JSON.stringify(savedContent) }]];
|
||||
if (imageOptions && sql.startsWith('UPDATE c_template_regions SET x')) {
|
||||
Object.assign(template.regions[0], { x: params[0], y: params[1], width: params[2], height: params[3] });
|
||||
}
|
||||
if (failUpdate && sql.startsWith('UPDATE c_template_regions')) throw new Error('Write failed');
|
||||
if (sql.startsWith('UPDATE c_template_regions SET animation_json')) {
|
||||
template.regions.find(region => region.id === params[2]).animation_json = params[0];
|
||||
@@ -496,13 +507,14 @@ function createSharedLayoutRouteHarness(failUpdate, allowSlideUpdate) {
|
||||
get(route, ...actions) { handlers['GET ' + route] = actions; registrations.push('GET ' + route); },
|
||||
post(route, ...actions) { handlers['POST ' + route] = actions; registrations.push('POST ' + route); }
|
||||
}, {
|
||||
pool: { async getConnection() { return connection; }, async query() { return [[{ slide_count: 3 }]]; } },
|
||||
common: { fetchTemplateById: async () => template, parseJsonSafe: JSON.parse }, pages: {},
|
||||
uploadDir: imageOptions && imageOptions.uploadDir,
|
||||
pool: { async getConnection() { return connection; }, async query(sql, params) { calls.push([sql, params]); return [[{ slide_count: 3 }]]; } },
|
||||
common: { fetchTemplateById: async () => imageOptions ? JSON.parse(JSON.stringify(template)) : template, parseJsonSafe: JSON.parse }, pages: {},
|
||||
upload: { any: () => noop, single: () => noop }, setAuthMessageCookie: noop,
|
||||
fetchScreensBySlideId: async () => [], fetchScreensByTemplateId: async () => [{ slug: 'lobby' }],
|
||||
collectUploadReferencesFromSlide: () => [], collectUploadReferencesFromTemplate: () => [],
|
||||
collectUploadReferencesFromPayload: () => [], removeUnusedUploadFiles: noop,
|
||||
syncPlaylistUploadsOnChange: noop, getAuditUserId: () => 9, redirectAfterSave: noop,
|
||||
collectUploadReferencesFromPayload: payload => imageOptions ? require('../src/data/content-references').collectContentReferences('slide', 42, JSON.parse(payload.contentJson)).filter(reference => reference.reference_type === 'media').map(reference => reference.reference_value) : [], removeUnusedUploadFiles: noop,
|
||||
syncPlaylistUploadsOnChange: async value => calls.push(['sync', value]), getAuditUserId: () => 9, redirectAfterSave: noop,
|
||||
notifyPlayerScreens: async (...args) => calls.push(['notify', ...args]), broadcastDashboardState: noop,
|
||||
getSlideDeleteBlockMessage: noop, getTemplateDeleteBlockMessage: noop, getCanvasSizeDeleteBlockMessage: noop,
|
||||
hasAnyPermission: () => Boolean(allowSlideUpdate),
|
||||
@@ -691,4 +703,65 @@ test('thumbnail markup uses template defaults for read-only regions', function (
|
||||
}, 'http://localhost');
|
||||
assert.match(html, /\/media\/brand\.png/);
|
||||
assert.doesNotMatch(html, /old-brand/);
|
||||
});
|
||||
});
|
||||
|
||||
test('shared layout save regenerates cropped playback images and syncs the new file', async () => {
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const uploadDir = await fs.mkdtemp(path.join(require('node:os').tmpdir(), 'pulse-layout-crop-'));
|
||||
try {
|
||||
await require('sharp')({ create: { width: 1200, height: 800, channels: 3, background: '#ff0000' } }).png().toFile(path.join(uploadDir, 'original.png'));
|
||||
const content = { type: 'image', value: '/media/uploads/small.png', original_value: '/media/uploads/original.png', crop: { x: 0, y: 0, width: 1200, height: 800 } };
|
||||
const { handlers, calls, template } = createSharedLayoutRouteHarness(false, true, { uploadDir, content });
|
||||
await handlers['POST /templates/:id/layout'].at(-1)({ params: { id: '7' }, body: {
|
||||
baseline: getTemplateLayoutSnapshot(template),
|
||||
regions: [{ region_key: 'hero', x: 0, y: 0, width: 900, height: 600, z_index: 1, lock_ratio: '' }]
|
||||
} }, { json() {} }, error => { throw error; });
|
||||
const write = calls.find(call => call[0].startsWith('UPDATE c_slides SET content_json'));
|
||||
const saved = JSON.parse(write[1][0]).hero;
|
||||
assert.equal(saved.original_value, content.original_value);
|
||||
assert.deepEqual(saved.crop, content.crop);
|
||||
const metadata = await require('sharp')(path.join(uploadDir, path.basename(saved.value))).metadata();
|
||||
assert.equal(metadata.width, 900);
|
||||
assert.equal(metadata.height, 600);
|
||||
assert.ok(calls.some(call => call[0].startsWith('UPDATE c_media_assets SET is_published')));
|
||||
assert.ok(calls.some(call => call[0] === 'sync' && call[1].nextUploadRefs.has(saved.value)));
|
||||
assert.ok(calls.some(call => call[0].startsWith('INSERT IGNORE INTO c_content_references')));
|
||||
} finally {
|
||||
await fs.rm(uploadDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
for (const uncropped of [false, true]) {
|
||||
test('shared image layouts resize ' + (uncropped ? 'uncropped' : 'cropped') + ' defaults but retain explicit slide geometry', async () => {
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const uploadDir = await fs.mkdtemp(path.join(require('node:os').tmpdir(), 'pulse-default-crop-'));
|
||||
try {
|
||||
await require('sharp')({ create: { width: 1200, height: 800, channels: 3, background: '#00ff00' } }).png().toFile(path.join(uploadDir, 'original.png'));
|
||||
const original = { type: 'image', value: '/media/uploads/old.png', original_value: '/media/uploads/original.png', crop: uncropped ? null : { x: 0, y: 0, width: 1200, height: 800 } };
|
||||
const { handlers, calls, template } = createSharedLayoutRouteHarness(false, true, {
|
||||
uploadDir, defaultContent: { ...original }, content: { ...original, layout: { x: 0, y: 0, width: 150, height: 100 } }
|
||||
});
|
||||
await handlers['POST /templates/:id/layout'].at(-1)({ params: { id: '7' }, body: {
|
||||
baseline: getTemplateLayoutSnapshot(template),
|
||||
regions: [{ region_key: 'hero', x: 0, y: 0, width: 900, height: 600, z_index: 1, lock_ratio: '' }]
|
||||
} }, { json() {} }, error => { throw error; });
|
||||
const defaultWrite = calls.find(call => call[0].startsWith('UPDATE c_template_regions SET default_content_json'));
|
||||
const defaultImage = JSON.parse(defaultWrite[1][0]);
|
||||
const defaultMetadata = await require('sharp')(path.join(uploadDir, path.basename(defaultImage.value))).metadata();
|
||||
assert.equal(defaultMetadata.width, 900);
|
||||
assert.equal(defaultMetadata.height, 600);
|
||||
const slideWrite = calls.find(call => call[0].startsWith('UPDATE c_slides SET content_json'));
|
||||
const slideImage = JSON.parse(slideWrite[1][0]).hero;
|
||||
const slideMetadata = await require('sharp')(path.join(uploadDir, path.basename(slideImage.value))).metadata();
|
||||
assert.equal(slideMetadata.width, 150);
|
||||
assert.equal(slideMetadata.height, 100);
|
||||
assert.equal(slideImage.layout.width, 150);
|
||||
assert.equal(defaultImage.original_value, original.original_value);
|
||||
assert.deepEqual(defaultImage.crop, original.crop);
|
||||
} finally {
|
||||
await fs.rm(uploadDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user