Release v2.13.1
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m15s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
2026-09-11 20:46:37 +01:00
parent 0a03cdd0b7
commit 394d23bb4d
47 changed files with 2408 additions and 559 deletions
+72 -3
View File
@@ -9,6 +9,29 @@ require('../src/common');
const { createUploadSyncService } = require('../src/web/lib/media');
test('upload reference collection includes WYSIWYG images across slide regions', () => {
const uploadSyncService = createUploadSyncService({
common: { parseJsonSafe: value => value ? JSON.parse(value) : null },
playerSnapshotCache: new Map(),
notifyPlayerScreens: async () => {}
});
assert.deepEqual(
Array.from(uploadSyncService.collectUploadReferencesFromPayload({
contentJson: JSON.stringify({
clock: { value: '<p><img src="http://localhost:8080/media/uploads/clock-image.png"></p>' },
html: { value: '<p><img src="/media/uploads/html-image.png"></p>' },
rss: { value: '<p><img src="/media/uploads/rss-image.png"></p>' }
})
})).sort(),
[
'/media/uploads/clock-image.png',
'/media/uploads/html-image.png',
'/media/uploads/rss-image.png'
].sort()
);
});
test('multipart uploads accept large text fields used by slide and template forms', async () => {
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-'));
const uploadSyncService = createUploadSyncService({
@@ -277,7 +300,7 @@ test('stale player registrations stop media sync retries and warnings', async ()
}
});
test('slide update sync removes uploads that were removed from the slide on the player', async () => {
test('slide update sync removes uploads only after the web-managed file is gone', async () => {
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-sync-slide-remove-'));
fs.mkdirSync(path.join(uploadDir, 'uploads'), { recursive: true });
fs.writeFileSync(path.join(uploadDir, 'uploads', 'keep.bin'), Buffer.from('keep'));
@@ -332,7 +355,7 @@ test('slide update sync removes uploads that were removed from the slide on the
return [[
{
identifier: 'player-one',
internal_base_url: 'http://player-one:8081',
internal_base_url: 'http://player:8081',
last_seen_at: liveLastSeenAt
}
]];
@@ -358,10 +381,23 @@ test('slide update sync removes uploads that were removed from the slide on the
});
assert.equal(fs.existsSync(path.join(uploadDir, 'uploads', 'keep.bin')), true);
assert.equal(fs.existsSync(path.join(uploadDir, 'uploads', 'remove.bin')), false);
assert.equal(fs.existsSync(path.join(uploadDir, 'uploads', 'remove.bin')), true);
assert.equal(fetchCalls.filter(function (call) {
return call.method === 'PUT';
}).length, 1);
assert.equal(fetchCalls.filter(function (call) {
return call.method === 'DELETE';
}).length, 0);
fs.unlinkSync(path.join(uploadDir, 'uploads', 'remove.bin'));
await uploadSyncService.syncPlaylistUploadsOnChange({
key: 'slide:update:124',
pool: {},
localUploadDir: uploadDir,
previousUploadRefs: ['/media/uploads/remove.bin'],
nextUploadRefs: []
});
assert.equal(fetchCalls.filter(function (call) {
return call.method === 'DELETE';
}).length, 1);
@@ -498,4 +534,37 @@ test('remote media sync uses the bridge device route', async () => {
global.fetch = originalFetch;
fs.rmSync(uploadDir, { recursive: true, force: true });
}
});
test('remote media deletion uses the bridge device route', async () => {
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-delete-bridge-'));
const activeLastSeenAt = new Date(Date.now() - 10_000).toISOString();
const fetchCalls = [];
const originalFetch = global.fetch;
global.fetch = async function (url, init) {
fetchCalls.push({ url, init });
return { ok: true, status: 200, statusText: 'OK', headers: { get() { return null; } }, async text() { return ''; } };
};
const uploadSyncService = createUploadSyncService({
common: {},
bridgeInternalBaseUrl: 'http://player-bridge:8090',
pool: {
async query() {
return [[{ identifier: 'player-remote', internal_base_url: 'https://remote-player.example', last_seen_at: activeLastSeenAt }]];
}
},
playerSnapshotCache: new Map(),
notifyPlayerScreens: async () => {}
});
try {
assert.equal(await uploadSyncService.removeUploadFileFromPlayer('/media/uploads/sample.bin', uploadDir), true);
assert.equal(fetchCalls[0].url, 'http://player-bridge:8090/api/media/uploads%2Fsample.bin');
assert.equal(fetchCalls[0].init.method, 'DELETE');
assert.equal(fetchCalls[0].init.headers['x-pulse-player-device-id'], 'player-remote');
} finally {
global.fetch = originalFetch;
fs.rmSync(uploadDir, { recursive: true, force: true });
}
});