Skip to content

Commit 4161ba6

Browse files
committed
Fix issue #31 (allow capitalization changes in space names). Add unit test coverage.
1 parent d397e5a commit 4161ba6

File tree

13 files changed

+704
-126
lines changed

13 files changed

+704
-126
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [1.1.8] - 2025-11-30
6+
7+
### Changes
8+
9+
- Fixed [issue #31](https://github.com/codedread/spaces/issues/31): Allow changing the capitalization of space names.
10+
- Increased unit test coverage from 16.17% to 22.69%.
11+
-
512
## [1.1.7] - 2025-10-01
613

714
### Changes

js/background/background.js

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import { dbService } from './dbService.js';
1111
import { spacesService } from './spacesService.js';
1212
import * as common from '../common.js';
13+
/** @typedef {common.SessionPresence} SessionPresence */
1314
/** @typedef {common.Space} Space */
1415
/** @typedef {import('./dbService.js').WindowBounds} WindowBounds */
1516

@@ -41,17 +42,17 @@ async function rediscoverWindowByUrl(storageKey, htmlFilename) {
4142

4243
// If not in storage or window doesn't exist, search for window by URL
4344
const targetUrl = chrome.runtime.getURL(htmlFilename);
44-
const allWindows = await chrome.windows.getAll({populate: true});
45-
45+
const allWindows = await chrome.windows.getAll({ populate: true });
46+
4647
for (const window of allWindows) {
4748
for (const tab of window.tabs) {
4849
if (tab.url && tab.url.startsWith(targetUrl)) {
49-
await chrome.storage.local.set({[storageKey]: window.id});
50+
await chrome.storage.local.set({ [storageKey]: window.id });
5051
return window.id;
5152
}
5253
}
5354
}
54-
55+
5556
return false;
5657
}
5758

@@ -166,19 +167,19 @@ export function initializeServiceWorker() {
166167
await closePopupWindow();
167168
}
168169
}
169-
170+
170171
spacesService.handleWindowFocussed(windowId);
171172
});
172173

173174
// Listen for window bounds changes (resize/move) with debouncing
174175
chrome.windows.onBoundsChanged.addListener(async (window) => {
175176
if (checkInternalSpacesWindows(window.id, false)) return;
176-
177+
177178
// Capture bounds - await ensures proper event ordering and timer management
178179
await spacesService.captureWindowBounds(window.id, {
179180
left: window.left,
180181
top: window.top,
181-
width: window.width,
182+
width: window.width,
182183
height: window.height
183184
});
184185
});
@@ -196,7 +197,7 @@ export function initializeServiceWorker() {
196197
try {
197198
// Ensure spacesService is initialized before processing any message
198199
await spacesService.ensureInitialized();
199-
200+
200201
const response = await processMessage(request, sender);
201202
if (response !== undefined) {
202203
sendResponse(response);
@@ -206,7 +207,7 @@ export function initializeServiceWorker() {
206207
sendResponse(false);
207208
}
208209
})();
209-
210+
210211
// We must return true synchronously to keep the message port open
211212
// for our async sendResponse() calls
212213
return true;
@@ -361,7 +362,7 @@ async function processMessage(request, sender) {
361362
if (!windowId) {
362363
return false;
363364
}
364-
365+
365366
try {
366367
const window = await chrome.windows.get(windowId);
367368
// Capture bounds before programmatically closing the window
@@ -755,7 +756,11 @@ function checkInternalSpacesWindows(windowId, windowClosed) {
755756
*/
756757
async function requestSessionPresence(sessionName) {
757758
const session = await dbService.fetchSessionByName(sessionName);
758-
return { exists: !!session, isOpen: !!session && !!session.windowId };
759+
return {
760+
exists: !!session,
761+
isOpen: !!session && !!session.windowId,
762+
sessionName: session?.name || false,
763+
};
759764
}
760765

761766
/**
@@ -798,7 +803,7 @@ async function requestSpaceFromWindowId(windowId) {
798803
};
799804
return space;
800805

801-
// otherwise build a space object out of the actual window
806+
// otherwise build a space object out of the actual window
802807
} else {
803808
try {
804809
const window = await chrome.windows.get(windowId, { populate: true });
@@ -827,11 +832,11 @@ async function requestSpaceFromWindowId(windowId) {
827832
*/
828833
async function requestSpaceFromSessionId(sessionId) {
829834
const session = await dbService.fetchSessionById(sessionId);
830-
835+
831836
if (!session) {
832837
return null;
833838
}
834-
839+
835840
return {
836841
sessionId: session.id,
837842
windowId: session.windowId,
@@ -893,12 +898,12 @@ async function handleLoadSession(sessionId, tabUrl) {
893898
await focusOrLoadTabInWindow(newWindow, tabUrl);
894899
}
895900

896-
/* session.tabs.forEach(function (curTab) {
897-
chrome.tabs.create({windowId: newWindow.id, url: curTab.url, pinned: curTab.pinned, active: false});
898-
});
901+
/* session.tabs.forEach(function (curTab) {
902+
chrome.tabs.create({windowId: newWindow.id, url: curTab.url, pinned: curTab.pinned, active: false});
903+
});
899904
900-
const tabs = await chrome.tabs.query({windowId: newWindow.id, index: 0});
901-
chrome.tabs.remove(tabs[0].id); */
905+
const tabs = await chrome.tabs.query({windowId: newWindow.id, index: 0});
906+
chrome.tabs.remove(tabs[0].id); */
902907
}
903908
}
904909

@@ -945,11 +950,11 @@ async function handleSaveNewSession(windowId, sessionName, deleteOld) {
945950
sessionName,
946951
curWindow.tabs,
947952
curWindow.id,
948-
{
949-
left: curWindow.left,
950-
top: curWindow.top,
951-
width: curWindow.width,
952-
height: curWindow.height
953+
{
954+
left: curWindow.left,
955+
top: curWindow.top,
956+
width: curWindow.width,
957+
height: curWindow.height
953958
},
954959
);
955960
return result ?? false;
@@ -977,7 +982,7 @@ async function handleRestoreFromBackup(space, deleteOld) {
977982
);
978983
return null;
979984
}
980-
985+
981986
// if we choose to overwrite, delete the existing session
982987
await handleDeleteSession(existingSession.id);
983988
}
@@ -1024,24 +1029,22 @@ async function handleImportNewSession(urlList) {
10241029
async function handleUpdateSessionName(sessionId, sessionName, deleteOld) {
10251030
// check to make sure session name doesn't already exist
10261031
const existingSession = await dbService.fetchSessionByName(sessionName);
1027-
// Fix: allow renaming when only capitalization is changed
1028-
if (existingSession && existingSession.id === sessionId) {
1029-
return spacesService.updateSessionName(sessionId, sessionName) ?? false;
1030-
}
10311032

1032-
// if session with same name already exist, then prompt to override the existing session
1033-
if (existingSession) {
1033+
// If a different session with same name already exists, then prompt to
1034+
// override the existing session.
1035+
// TESTME: Only delete if session ids differ.
1036+
if (existingSession && existingSession.id !== sessionId) {
10341037
if (!deleteOld) {
10351038
console.error(
10361039
`handleUpdateSessionName: Session with name "${sessionName}" already exists and deleteOld was not true.`
10371040
);
10381041
return false;
10391042
}
1040-
1043+
10411044
// if we choose to override, then delete the existing session
10421045
await handleDeleteSession(existingSession.id);
10431046
}
1044-
1047+
10451048
return spacesService.updateSessionName(sessionId, sessionName) ?? false;
10461049
}
10471050

@@ -1188,7 +1191,7 @@ async function handleMoveTabToSession(tabId, sessionId) {
11881191
if (!session) {
11891192
return false;
11901193
}
1191-
1194+
11921195
// if session is currently open then move it directly
11931196
if (session.windowId) {
11941197
moveTabToWindow(tab, session.windowId);
@@ -1345,7 +1348,7 @@ async function getTargetDisplayWorkArea() {
13451348
const activeDisplay = displays.find(display => {
13461349
const d = display.workArea;
13471350
return windowCenterX >= d.left && windowCenterX < (d.left + d.width) &&
1348-
windowCenterY >= d.top && windowCenterY < (d.top + d.height);
1351+
windowCenterY >= d.top && windowCenterY < (d.top + d.height);
13491352
});
13501353
if (activeDisplay) {
13511354
targetDisplay = activeDisplay;
@@ -1402,5 +1405,6 @@ export {
14021405
getEffectiveTabUrl,
14031406
getTargetDisplayWorkArea,
14041407
handleLoadSession,
1408+
handleUpdateSessionName,
14051409
requestAllSpaces,
14061410
};

js/background/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import { initializeServiceWorker } from './background.js';
22

3+
console.log(`Spaces ${chrome.runtime.getManifest().version}`)
34
initializeServiceWorker();

js/common.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* @typedef SessionPresence
2323
* @property {boolean} exists A session with this name exists in the database.
2424
* @property {boolean} isOpen The session is currently open in a window.
25+
* @property {string|false} sessionName The name of the session, or false if not named.
2526
*/
2627

2728
/**

js/popup.js

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ let globalUrl;
2525
let globalWindowId;
2626
let globalSessionName;
2727

28+
export function setGlobalCurrentSpace(space) {
29+
globalCurrentSpace = space;
30+
}
31+
2832
/** Initialize the popup window. */
2933
function initializePopup() {
3034
document.addEventListener('DOMContentLoaded', async () => {
@@ -77,8 +81,8 @@ function renderCommon() {
7781
document.getElementById(
7882
'activeSpaceTitle'
7983
).value = globalCurrentSpace.name
80-
? globalCurrentSpace.name
81-
: UNSAVED_SESSION;
84+
? globalCurrentSpace.name
85+
: UNSAVED_SESSION;
8286

8387
document.querySelector('body').onkeyup = e => {
8488
// listen for escape key
@@ -126,12 +130,12 @@ function handleCloseAction() {
126130

127131
async function renderMainCard() {
128132
const hotkeys = await requestHotkeys();
129-
document.querySelector(
130-
'#switcherLink .hotkey'
131-
).innerHTML = hotkeys.switchCode ? hotkeys.switchCode : NO_HOTKEY;
132-
document.querySelector(
133-
'#moverLink .hotkey'
134-
).innerHTML = hotkeys.moveCode ? hotkeys.moveCode : NO_HOTKEY;
133+
document.querySelector(
134+
'#switcherLink .hotkey'
135+
).innerHTML = hotkeys.switchCode ? hotkeys.switchCode : NO_HOTKEY;
136+
document.querySelector(
137+
'#moverLink .hotkey'
138+
).innerHTML = hotkeys.moveCode ? hotkeys.moveCode : NO_HOTKEY;
135139

136140
const hotkeyEls = document.querySelectorAll('.hotkey');
137141
for (let i = 0; i < hotkeyEls.length; i += 1) {
@@ -190,7 +194,8 @@ function handleNameEdit() {
190194
}
191195
}
192196

193-
async function handleNameSave() {
197+
export async function handleNameSave() {
198+
/** @type {HTMLInputElement} */
194199
const inputEl = document.getElementById('activeSpaceTitle');
195200
const newName = inputEl.value;
196201

@@ -200,14 +205,19 @@ async function handleNameSave() {
200205
return;
201206
}
202207

203-
if (
204-
newName === UNSAVED_SESSION ||
205-
newName === globalCurrentSpace.name
206-
) {
208+
// If the session is unnamed or the name has not changed, do nothing.
209+
if (newName === UNSAVED_SESSION || newName === globalCurrentSpace.name) {
207210
return;
208211
}
209212

210-
const canOverwrite = await checkSessionOverwrite(newName);
213+
// Spaces are looked up in the database by case-insensitive name. That means we do not allow
214+
// two spaces to have case-insensitive identical names (e.g. "main" and "Main"). If the new
215+
// name is a case-insensitive match of the previous name of the current session, we do not need
216+
// to check for overwrite, we just let the capitalization change happen.
217+
// TESTME: Save should occur when the name is a case-insensitive match of the previous name and
218+
// the requestSessionPresence message should not be sent.
219+
const caseInsensitiveMatch = globalCurrentSpace.name.toLowerCase() === newName.toLowerCase();
220+
const canOverwrite = caseInsensitiveMatch || await checkSessionOverwrite(newName);
211221
if (!canOverwrite) {
212222
inputEl.value = globalCurrentSpace.name || UNSAVED_SESSION;
213223
inputEl.blur();
@@ -246,7 +256,7 @@ async function renderSwitchCard() {
246256
document.getElementById(
247257
'popupContainer'
248258
).innerHTML = document.getElementById('switcherTemplate').innerHTML;
249-
259+
250260
const spaces = await chrome.runtime.sendMessage({ action: 'requestAllSpaces' });
251261
spacesRenderer.initialise(8, true);
252262
spacesRenderer.renderSpaces(spaces);
@@ -367,7 +377,7 @@ async function updateTabDetails() {
367377
action: 'requestTabDetail',
368378
tabId: globalTabId,
369379
});
370-
380+
371381
if (tab) {
372382
nodes.activeTabTitle.innerHTML = escapeHtml(tab.title);
373383

@@ -397,9 +407,9 @@ async function updateTabDetails() {
397407
const cleanUrl =
398408
globalUrl.indexOf('://') > 0
399409
? globalUrl.substr(
400-
globalUrl.indexOf('://') + 3,
401-
globalUrl.length
402-
)
410+
globalUrl.indexOf('://') + 3,
411+
globalUrl.length
412+
)
403413
: globalUrl;
404414
nodes.activeTabTitle.innerHTML = escapeHtml(cleanUrl);
405415
nodes.activeTabFavicon.setAttribute('src', '/img/new.png');

0 commit comments

Comments
 (0)