Scope inbox and activity by workspace

This commit is contained in:
2026-06-28 03:35:35 +08:00
parent 2c19dd2132
commit b4ef3b38b0
4 changed files with 342 additions and 41 deletions
+35 -9
View File
@@ -136,8 +136,8 @@ function makeApi(initialSettings = {}) {
};
},
},
storedEvents() {
return settings.events || [];
storedEvents(key = 'events') {
return settings[key] || [];
},
};
}
@@ -146,10 +146,10 @@ async function flush() {
for (let i = 0; i < 10; i += 1) await Promise.resolve();
}
async function mountWithApi(api, document = makeDocument()) {
async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, workspaceRootPath: 'Project' }, document = makeDocument()) {
const component = loadComponent(document);
const container = new FakeNode('div');
component.mount(container, { workspaceNode: { name: 'Project' } }, api);
component.mount(container, props, api);
await flush();
return { component, container, document };
}
@@ -176,31 +176,57 @@ async function mountWithApi(api, document = makeDocument()) {
});
await flush();
const stored = api.storedEvents();
const projectKey = 'events:workspace:Project';
const clientKey = 'events:workspace:ClientA';
const globalKey = 'events:global';
const stored = api.storedEvents(projectKey);
if (stored.length !== 1) throw new Error(`expected one stored activity event, got ${stored.length}`);
if (stored[0].type !== 'browser.capture.selection') throw new Error('stored event type mismatch');
if (stored[0].sourcePluginId !== 'verstak.browser-inbox') throw new Error('stored event source plugin mismatch');
if (api.storedEvents(globalKey).length !== 0) throw new Error('workspace activity leaked into global storage');
if (!container.textContent.includes('Example Article')) throw new Error('browser capture title was not rendered');
if (!container.textContent.includes('browser.capture.selection')) throw new Error('event type was not rendered');
const clientView = await mountWithApi(api, { workspaceNode: { name: 'ClientA' }, workspaceRootPath: 'ClientA' });
if (clientView.container.textContent.includes('Example Article')) throw new Error('Project activity leaked into ClientA workspace view');
await api.handlers['note.saved']({
name: 'note.saved',
pluginId: 'verstak.notes',
timestamp: '2026-06-27T00:10:00Z',
payload: {
title: 'Client note',
path: 'ClientA/Notes/Client.md',
workspaceRootPath: 'ClientA',
},
});
await flush();
if (api.storedEvents(clientKey).length !== 1) throw new Error('ClientA activity was not stored under ClientA workspace key');
if (!clientView.container.textContent.includes('Client note')) throw new Error('ClientA activity was not rendered');
component.unmount && component.unmount(clientView.container);
const globalView = await mountWithApi(api, {});
if (!globalView.container.textContent.includes('Example Article')) throw new Error('global activity did not aggregate Project activity');
if (!globalView.container.textContent.includes('Client note')) throw new Error('global activity did not aggregate ClientA activity');
component.unmount && component.unmount(globalView.container);
const manualButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'manual');
if (!manualButton) throw new Error('manual activity button not found');
manualButton.click();
await flush();
if (api.storedEvents().length !== 2) throw new Error('manual activity was not stored');
if (api.storedEvents(projectKey).length !== 2) throw new Error('manual activity was not stored');
if (!container.textContent.includes('Manual activity')) throw new Error('manual activity was not rendered');
const clearButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'clear');
if (!clearButton) throw new Error('clear activity button not found');
clearButton.click();
await flush();
if (api.storedEvents().length !== 0) throw new Error('clear action did not remove activity events');
if (api.storedEvents(projectKey).length !== 0) throw new Error('clear action did not remove activity events');
component.unmount && component.unmount(container);
if (api.unsubscribed.length !== 9) throw new Error(`expected 9 unsubscribers, got ${api.unsubscribed.length}`);
if (api.unsubscribed.length !== 27) throw new Error(`expected 27 unsubscribers, got ${api.unsubscribed.length}`);
const persistedApi = makeApi({
events: [{
'events:workspace:Project': [{
activityId: 'persisted-1',
type: 'note.saved',
title: 'Saved note',
+46 -9
View File
@@ -154,8 +154,8 @@ function makeApi(initialSettings = {}) {
return { ...settings };
},
},
getStoredCaptures() {
return settings.captures || [];
getStoredCaptures(key = 'captures') {
return settings[key] || [];
},
};
}
@@ -164,10 +164,10 @@ async function flush() {
for (let i = 0; i < 8; i += 1) await Promise.resolve();
}
async function mountWithApi(api, document = makeDocument()) {
async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, workspaceRootPath: 'Project' }, document = makeDocument()) {
const component = loadComponent(document);
const container = new FakeNode('div');
component.mount(container, { workspaceNode: { name: 'Project' } }, api);
component.mount(container, props, api);
await flush();
return { component, container, document };
}
@@ -196,9 +196,13 @@ async function mountWithApi(api, document = makeDocument()) {
});
await flush();
const captures = api.getStoredCaptures();
const projectKey = 'captures:workspace:Project';
const clientKey = 'captures:workspace:ClientA';
const globalKey = 'captures:global';
const captures = api.getStoredCaptures(projectKey);
if (captures.length !== 1) throw new Error(`expected one stored capture, got ${captures.length}`);
if (captures[0].captureId !== 'capture-1') throw new Error('stored capture id mismatch');
if (api.getStoredCaptures(globalKey).length !== 0) throw new Error('workspace capture leaked into global storage');
const row = walk(container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1');
if (!row) throw new Error('capture row was not rendered');
@@ -220,18 +224,51 @@ async function mountWithApi(api, document = makeDocument()) {
},
});
await flush();
if (api.getStoredCaptures().length !== 1) throw new Error('duplicate capture was stored');
if (api.getStoredCaptures(projectKey).length !== 1) throw new Error('duplicate capture was stored');
const clientView = await mountWithApi(api, { workspaceNode: { name: 'ClientA' }, workspaceRootPath: 'ClientA' });
if (walk(clientView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1')) {
throw new Error('Project capture leaked into ClientA workspace view');
}
await api.handlers['browser.capture.page']({
name: 'browser.capture.page',
timestamp: '2026-06-27T00:10:00Z',
payload: {
captureId: 'capture-2',
capturedAt: '2026-06-27T00:10:00.000Z',
kind: 'page',
url: 'https://client.example.com/',
title: 'Client Page',
domain: 'client.example.com',
workspaceRootPath: 'ClientA',
},
});
await flush();
if (api.getStoredCaptures(clientKey).length !== 1) throw new Error('ClientA capture was not stored under ClientA workspace key');
if (!walk(clientView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-2')) {
throw new Error('ClientA capture was not rendered');
}
component.unmount && component.unmount(clientView.container);
const globalView = await mountWithApi(api, {});
if (!walk(globalView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1')) {
throw new Error('global browser inbox did not aggregate Project capture');
}
if (!walk(globalView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-2')) {
throw new Error('global browser inbox did not aggregate ClientA capture');
}
component.unmount && component.unmount(globalView.container);
const clearButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'clear');
if (!clearButton) throw new Error('clear button not found');
clearButton.click();
await flush();
if (api.getStoredCaptures().length !== 0) throw new Error('clear action did not empty stored captures');
if (api.getStoredCaptures(projectKey).length !== 0) throw new Error('clear action did not empty stored captures');
component.unmount && component.unmount(container);
if (api.unsubscribed.length !== 3) throw new Error('component did not unsubscribe all capture handlers');
if (api.unsubscribed.length !== 9) throw new Error('component did not unsubscribe all capture handlers');
const persistedApi = makeApi({ captures: [captures[0]] });
const persistedApi = makeApi({ 'captures:workspace:Project': [captures[0]] });
const persisted = await mountWithApi(persistedApi);
if (!walk(persisted.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1')) {
throw new Error('persisted capture was not rendered on mount');