diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 7a187cd..6091f03 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -572,6 +572,16 @@ function buildOfflineTileHTML(source) { ); } +/** + * Open a login popup window for a remote muxplex instance. + * Strips trailing slashes from remoteUrl before appending /login. + * @param {string} remoteUrl - The base URL of the remote instance + */ +function openLoginPopup(remoteUrl) { + var baseUrl = remoteUrl.replace(/\/+$/, ''); + window.open(baseUrl + '/login', '_blank', 'width=500,height=600'); +} + /** * Returns sessions with hidden session names removed. * Only hides LOCAL sessions (those with empty/absent sourceUrl) matching the @@ -1874,6 +1884,14 @@ function bindStaticEventListeners() { if (name) killSession(name); }); + document.addEventListener('click', function(e) { + var loginBtn = e.target.closest && e.target.closest('.source-tile__login-btn'); + if (!loginBtn) return; + e.stopPropagation(); + var url = loginBtn.dataset.url; + if (url) openLoginPopup(url); + }); + on($('back-btn'), 'click', closeSession); var newSessionBtn = $('new-session-btn'); if (newSessionBtn) on(newSessionBtn, 'click', function() { showNewSessionInput(newSessionBtn); }); @@ -2262,6 +2280,7 @@ if (typeof module !== 'undefined' && module.exports) { // Federation tiles buildAuthTileHTML, buildOfflineTileHTML, + openLoginPopup, formatLastSeen, // Test-only helpers _setCurrentSessions, diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index f7d23f7..5b50a6a 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -2977,3 +2977,49 @@ test('formatLastSeen returns days ago for diff >= 86400', () => { assert.match(app.formatLastSeen(twoDaysAgo), /^\d+d ago$/, 'should return Xd ago for diff >= 86400s'); }); +// --- openLoginPopup (task-5-login-popup-flow) --- + +test('openLoginPopup is exported as a function', () => { + assert.strictEqual(typeof app.openLoginPopup, 'function', 'openLoginPopup should be exported as a function'); +}); + +test('openLoginPopup calls window.open with correct URL and dimensions', () => { + const openCalls = []; + const origOpen = globalThis.window.open; + globalThis.window.open = (url, target, features) => { openCalls.push({ url, target, features }); }; + + app.openLoginPopup('http://work:8088'); + + globalThis.window.open = origOpen; + + assert.strictEqual(openCalls.length, 1, 'window.open should be called exactly once'); + assert.strictEqual(openCalls[0].url, 'http://work:8088/login', 'url should be remoteUrl + /login'); + assert.strictEqual(openCalls[0].target, '_blank', 'target should be _blank'); + assert.ok(openCalls[0].features.includes('width=500'), 'features should include width=500'); + assert.ok(openCalls[0].features.includes('height=600'), 'features should include height=600'); +}); + +test('openLoginPopup appends /login to URL without trailing slash', () => { + const openCalls = []; + const origOpen = globalThis.window.open; + globalThis.window.open = (url) => { openCalls.push(url); }; + + app.openLoginPopup('http://work:8088'); + + globalThis.window.open = origOpen; + + assert.strictEqual(openCalls[0], 'http://work:8088/login', 'should append /login when no trailing slash'); +}); + +test('openLoginPopup handles URL with trailing slash', () => { + const openCalls = []; + const origOpen = globalThis.window.open; + globalThis.window.open = (url) => { openCalls.push(url); }; + + app.openLoginPopup('http://work:8088/'); + + globalThis.window.open = origOpen; + + assert.strictEqual(openCalls[0], 'http://work:8088/login', 'should strip trailing slash then append /login'); +}); +