Add baseline activity plugin
This commit is contained in:
parent
59e48d7ea4
commit
47ae318094
|
|
@ -0,0 +1,325 @@
|
|||
/* ===========================================================
|
||||
Activity Plugin - Verstak v2 Frontend Bundle
|
||||
Contract: window.VerstakPluginRegister(id, { components })
|
||||
=========================================================== */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var PLUGIN_ID = 'verstak.activity';
|
||||
var MAX_EVENTS = 250;
|
||||
var ACTIVITY_EVENTS = [
|
||||
'file.opened',
|
||||
'file.changed',
|
||||
'note.saved',
|
||||
'action.started',
|
||||
'browser.capture.received',
|
||||
'case.selected',
|
||||
'browser.capture.page',
|
||||
'browser.capture.selection',
|
||||
'browser.capture.link'
|
||||
];
|
||||
|
||||
function injectStyles() {
|
||||
if (document.getElementById('activity-style-injected')) return;
|
||||
var style = document.createElement('style');
|
||||
style.id = 'activity-style-injected';
|
||||
style.textContent = STYLES;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
var STYLES = [
|
||||
'.activity-root{display:flex;flex-direction:column;height:100%;min-height:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a}',
|
||||
'.activity-toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid #16213e;background:#12122a;flex-shrink:0;flex-wrap:wrap}',
|
||||
'.activity-title{font-size:.84rem;font-weight:600;color:#e0e0e0}',
|
||||
'.activity-count{font-size:.72rem;color:#8b8ba8}',
|
||||
'.activity-spacer{flex:1}',
|
||||
'.activity-btn{font-size:.78rem;padding:.32rem .65rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}',
|
||||
'.activity-btn:hover{background:#2a2a4e;border-color:#4ecca3}',
|
||||
'.activity-btn:disabled{opacity:.45;cursor:default}',
|
||||
'.activity-btn.danger{border-color:#633;color:#ff9a9a}',
|
||||
'.activity-status{font-size:.72rem;color:#8b8ba8;white-space:nowrap}',
|
||||
'.activity-status.error{color:#e74c3c}',
|
||||
'.activity-list{flex:1;min-height:0;overflow:auto;background:#101020}',
|
||||
'.activity-empty{height:100%;display:flex;align-items:center;justify-content:center;color:#666;font-size:.86rem;padding:2rem;text-align:center}',
|
||||
'.activity-row{display:grid;grid-template-columns:9.5rem minmax(0,1fr);gap:.75rem;padding:.72rem .85rem;border-bottom:1px solid rgba(22,33,62,.6)}',
|
||||
'.activity-time{font-size:.72rem;color:#777;white-space:nowrap}',
|
||||
'.activity-main{min-width:0}',
|
||||
'.activity-row-head{display:flex;align-items:center;gap:.45rem;min-width:0}',
|
||||
'.activity-type{font-size:.68rem;color:#4ecca3;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}',
|
||||
'.activity-title-text{font-size:.86rem;color:#e0e0e0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.activity-summary{margin-top:.25rem;font-size:.78rem;line-height:1.4;color:#aaa;white-space:pre-wrap;overflow-wrap:anywhere}',
|
||||
'.activity-source{margin-top:.25rem;font-size:.72rem;color:#777}',
|
||||
'@media(max-width:760px){.activity-row{grid-template-columns:1fr;gap:.25rem}.activity-toolbar{align-items:stretch}.activity-status{width:100%}}'
|
||||
].join('\n');
|
||||
|
||||
function el(tag, attrs, children) {
|
||||
var elem = document.createElement(tag);
|
||||
if (attrs) {
|
||||
Object.keys(attrs).forEach(function (k) {
|
||||
if (attrs[k] == null) return;
|
||||
if (k === 'className') elem.className = attrs[k];
|
||||
else if (k.slice(0, 2) === 'on') elem.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
|
||||
else if (k === 'textContent') elem.textContent = attrs[k];
|
||||
else elem.setAttribute(k, attrs[k]);
|
||||
});
|
||||
}
|
||||
if (children) {
|
||||
(Array.isArray(children) ? children : [children]).forEach(function (child) {
|
||||
if (child == null) return;
|
||||
elem.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
|
||||
});
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
return String(value == null ? '' : value);
|
||||
}
|
||||
|
||||
function activityId() {
|
||||
return 'activity-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
function eventPayload(event) {
|
||||
return event && event.payload && typeof event.payload === 'object' ? event.payload : {};
|
||||
}
|
||||
|
||||
function firstText(values) {
|
||||
for (var i = 0; i < values.length; i += 1) {
|
||||
var value = text(values[i]).trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function titleFromEvent(event, payload) {
|
||||
return firstText([
|
||||
payload.title,
|
||||
payload.name,
|
||||
payload.path,
|
||||
payload.url,
|
||||
payload.captureId,
|
||||
event.name,
|
||||
'Activity event'
|
||||
]);
|
||||
}
|
||||
|
||||
function summaryFromEvent(event, payload) {
|
||||
if (payload.text) return text(payload.text).trim();
|
||||
return firstText([
|
||||
payload.summary,
|
||||
payload.description,
|
||||
payload.path,
|
||||
payload.url,
|
||||
payload.domain,
|
||||
event.name
|
||||
]);
|
||||
}
|
||||
|
||||
function eventToActivity(event) {
|
||||
var payload = eventPayload(event);
|
||||
return {
|
||||
activityId: activityId(),
|
||||
type: text(event && event.name).trim() || 'activity.event',
|
||||
title: titleFromEvent(event || {}, payload),
|
||||
summary: summaryFromEvent(event || {}, payload),
|
||||
occurredAt: text(payload.occurredAt || payload.capturedAt || (event && event.timestamp) || new Date().toISOString()),
|
||||
receivedAt: new Date().toISOString(),
|
||||
sourcePluginId: text((event && event.pluginId) || payload.pluginId || payload.sourcePluginId),
|
||||
payload: payload
|
||||
};
|
||||
}
|
||||
|
||||
function manualActivity() {
|
||||
return {
|
||||
activityId: activityId(),
|
||||
type: 'activity.manual',
|
||||
title: 'Manual activity',
|
||||
summary: 'Manually recorded activity event',
|
||||
occurredAt: new Date().toISOString(),
|
||||
receivedAt: new Date().toISOString(),
|
||||
sourcePluginId: PLUGIN_ID,
|
||||
payload: {}
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStoredEvents(value) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter(function (item) {
|
||||
return item && typeof item === 'object' && item.activityId;
|
||||
}).map(function (item) {
|
||||
return {
|
||||
activityId: text(item.activityId),
|
||||
type: text(item.type || item.name || 'activity.event'),
|
||||
title: text(item.title || item.type || 'Activity event'),
|
||||
summary: text(item.summary),
|
||||
occurredAt: text(item.occurredAt || item.timestamp || item.receivedAt),
|
||||
receivedAt: text(item.receivedAt),
|
||||
sourcePluginId: text(item.sourcePluginId || item.pluginId),
|
||||
payload: item.payload && typeof item.payload === 'object' ? item.payload : {}
|
||||
};
|
||||
}).slice(0, MAX_EVENTS);
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '';
|
||||
var date = new Date(value);
|
||||
if (isNaN(date.getTime())) return text(value);
|
||||
return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function ActivityView() {}
|
||||
|
||||
ActivityView.mount = function (containerEl, props, api) {
|
||||
injectStyles();
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.className = 'activity-root';
|
||||
containerEl.setAttribute('data-plugin-id', PLUGIN_ID);
|
||||
|
||||
var events = [];
|
||||
var statusText = 'Loading activity...';
|
||||
var statusClass = '';
|
||||
var disposed = false;
|
||||
var unsubscribers = [];
|
||||
|
||||
var toolbar = el('div', { className: 'activity-toolbar' });
|
||||
var titleEl = el('span', { className: 'activity-title', textContent: 'Activity' });
|
||||
var countEl = el('span', { className: 'activity-count' });
|
||||
var statusEl = el('span', { className: 'activity-status' });
|
||||
var manualBtn = el('button', {
|
||||
className: 'activity-btn',
|
||||
'data-activity-action': 'manual',
|
||||
textContent: 'Record',
|
||||
onClick: function () {
|
||||
addActivity(manualActivity());
|
||||
}
|
||||
});
|
||||
var clearBtn = el('button', {
|
||||
className: 'activity-btn danger',
|
||||
'data-activity-action': 'clear',
|
||||
textContent: 'Clear',
|
||||
onClick: function () {
|
||||
events = [];
|
||||
persist().then(render);
|
||||
}
|
||||
});
|
||||
toolbar.appendChild(titleEl);
|
||||
toolbar.appendChild(countEl);
|
||||
toolbar.appendChild(el('span', { className: 'activity-spacer' }));
|
||||
toolbar.appendChild(statusEl);
|
||||
toolbar.appendChild(manualBtn);
|
||||
toolbar.appendChild(clearBtn);
|
||||
|
||||
var listEl = el('div', { className: 'activity-list' });
|
||||
containerEl.appendChild(toolbar);
|
||||
containerEl.appendChild(listEl);
|
||||
|
||||
function persist() {
|
||||
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
||||
return api.settings.write('events', events).catch(function (err) {
|
||||
statusText = 'Could not save activity: ' + (err && err.message ? err.message : String(err));
|
||||
statusClass = 'error';
|
||||
});
|
||||
}
|
||||
|
||||
function addActivity(activity) {
|
||||
events = [activity].concat(events).slice(0, MAX_EVENTS);
|
||||
statusText = 'Activity recorded';
|
||||
statusClass = '';
|
||||
return persist().then(render);
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
listEl.innerHTML = '';
|
||||
if (events.length === 0) {
|
||||
listEl.appendChild(el('div', { className: 'activity-empty', textContent: 'No activity events yet.' }));
|
||||
return;
|
||||
}
|
||||
events.forEach(function (activity) {
|
||||
listEl.appendChild(el('div', {
|
||||
className: 'activity-row',
|
||||
'data-activity-id': activity.activityId
|
||||
}, [
|
||||
el('div', { className: 'activity-time', textContent: formatDate(activity.occurredAt) || '-' }),
|
||||
el('div', { className: 'activity-main' }, [
|
||||
el('div', { className: 'activity-row-head' }, [
|
||||
el('span', { className: 'activity-type', textContent: activity.type }),
|
||||
el('span', { className: 'activity-title-text', textContent: activity.title || 'Activity event' })
|
||||
]),
|
||||
activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null,
|
||||
activity.sourcePluginId ? el('div', { className: 'activity-source', textContent: activity.sourcePluginId }) : null
|
||||
])
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
countEl.textContent = events.length + ' event' + (events.length === 1 ? '' : 's');
|
||||
clearBtn.disabled = events.length === 0;
|
||||
statusEl.textContent = statusText;
|
||||
statusEl.className = 'activity-status' + (statusClass ? ' ' + statusClass : '');
|
||||
renderList();
|
||||
}
|
||||
|
||||
function loadStored() {
|
||||
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
||||
return api.settings.read('events').then(function (stored) {
|
||||
events = normalizeStoredEvents(stored);
|
||||
}).catch(function (err) {
|
||||
statusText = 'Could not load activity: ' + (err && err.message ? err.message : String(err));
|
||||
statusClass = 'error';
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeEvents() {
|
||||
if (!api || !api.events || typeof api.events.subscribe !== 'function') return Promise.resolve();
|
||||
return Promise.all(ACTIVITY_EVENTS.map(function (eventName) {
|
||||
return api.events.subscribe(eventName, function (event) {
|
||||
return addActivity(eventToActivity(event));
|
||||
}).then(function (unsubscribe) {
|
||||
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
||||
});
|
||||
})).then(function () {
|
||||
statusText = 'Listening for activity';
|
||||
statusClass = '';
|
||||
}).catch(function (err) {
|
||||
statusText = 'Activity subscriptions unavailable: ' + (err && err.message ? err.message : String(err));
|
||||
statusClass = 'error';
|
||||
});
|
||||
}
|
||||
|
||||
render();
|
||||
loadStored().then(function () {
|
||||
if (disposed) return;
|
||||
render();
|
||||
return subscribeEvents();
|
||||
}).then(function () {
|
||||
if (!disposed) render();
|
||||
});
|
||||
|
||||
containerEl.__activityUnmount = function () {
|
||||
disposed = true;
|
||||
while (unsubscribers.length > 0) {
|
||||
try {
|
||||
unsubscribers.pop()();
|
||||
} catch (err) {
|
||||
console.error('[Activity] unsubscribe error:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
ActivityView.unmount = function (containerEl) {
|
||||
if (containerEl && typeof containerEl.__activityUnmount === 'function') {
|
||||
containerEl.__activityUnmount();
|
||||
delete containerEl.__activityUnmount;
|
||||
}
|
||||
};
|
||||
|
||||
window.VerstakPluginRegister(PLUGIN_ID, {
|
||||
components: {
|
||||
ActivityView: ActivityView
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "verstak.activity",
|
||||
"name": "Activity",
|
||||
"version": "0.1.0",
|
||||
"apiVersion": "0.1.0",
|
||||
"description": "Workspace-scoped activity log for public plugin events.",
|
||||
"source": "official",
|
||||
"icon": "activity",
|
||||
"provides": [
|
||||
"activity.log",
|
||||
"activity.provider",
|
||||
"activity.reconstruction"
|
||||
],
|
||||
"permissions": [
|
||||
"events.subscribe",
|
||||
"storage.namespace",
|
||||
"ui.register"
|
||||
],
|
||||
"frontend": {
|
||||
"entry": "frontend/src/index.js"
|
||||
},
|
||||
"contributes": {
|
||||
"workspaceItems": [
|
||||
{
|
||||
"id": "verstak.activity.workspace",
|
||||
"title": "Activity",
|
||||
"icon": "activity",
|
||||
"component": "ActivityView"
|
||||
}
|
||||
],
|
||||
"activityProviders": [
|
||||
{
|
||||
"id": "verstak.activity.log",
|
||||
"events": [
|
||||
"file.opened",
|
||||
"file.changed",
|
||||
"note.saved",
|
||||
"action.started",
|
||||
"browser.capture.received",
|
||||
"case.selected",
|
||||
"browser.capture.page",
|
||||
"browser.capture.selection",
|
||||
"browser.capture.link"
|
||||
],
|
||||
"handler": "recordActivityEvent"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -217,6 +217,8 @@ if command -v node &>/dev/null; then
|
|||
report "file-preview frontend behavior" $?
|
||||
node "$ROOT/scripts/smoke-files-plugin.js"
|
||||
report "files frontend behavior" $?
|
||||
node "$ROOT/scripts/smoke-activity-plugin.js"
|
||||
report "activity frontend behavior" $?
|
||||
node "$ROOT/scripts/smoke-browser-inbox-plugin.js"
|
||||
report "browser inbox frontend behavior" $?
|
||||
node "$ROOT/scripts/smoke-search-plugin.js"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const sourcePath = path.join(root, 'plugins', 'activity', 'frontend', 'src', 'index.js');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
|
||||
class FakeNode {
|
||||
constructor(tagName) {
|
||||
this.tagName = String(tagName || '').toUpperCase();
|
||||
this.children = [];
|
||||
this.attributes = {};
|
||||
this.listeners = {};
|
||||
this.className = '';
|
||||
this.value = '';
|
||||
this.disabled = false;
|
||||
this.parentNode = null;
|
||||
this._textContent = '';
|
||||
this._innerHTML = '';
|
||||
}
|
||||
|
||||
appendChild(node) {
|
||||
this.children.push(node);
|
||||
node.parentNode = this;
|
||||
return node;
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = String(value);
|
||||
}
|
||||
|
||||
getAttribute(name) {
|
||||
return this.attributes[name];
|
||||
}
|
||||
|
||||
addEventListener(type, handler) {
|
||||
this.listeners[type] = this.listeners[type] || [];
|
||||
this.listeners[type].push(handler);
|
||||
}
|
||||
|
||||
dispatchEvent(type, event = {}) {
|
||||
(this.listeners[type] || []).forEach((handler) => handler({ target: this, preventDefault() {}, stopPropagation() {}, ...event }));
|
||||
}
|
||||
|
||||
click() {
|
||||
this.dispatchEvent('click');
|
||||
}
|
||||
|
||||
set innerHTML(value) {
|
||||
this._innerHTML = String(value || '');
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
get innerHTML() {
|
||||
return this._innerHTML + this.children.map((child) => child.innerHTML).join('');
|
||||
}
|
||||
|
||||
set textContent(value) {
|
||||
this._textContent = String(value || '');
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
get textContent() {
|
||||
if (this.tagName === '#TEXT') return this._textContent;
|
||||
return this._textContent + this.children.map((child) => child.textContent).join('');
|
||||
}
|
||||
}
|
||||
|
||||
function walk(node, fn) {
|
||||
if (fn(node)) return node;
|
||||
for (const child of node.children) {
|
||||
const found = walk(child, fn);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function makeDocument() {
|
||||
return {
|
||||
body: new FakeNode('body'),
|
||||
head: new FakeNode('head'),
|
||||
createElement(tagName) {
|
||||
return new FakeNode(tagName);
|
||||
},
|
||||
createTextNode(text) {
|
||||
const node = new FakeNode('#text');
|
||||
node.textContent = text;
|
||||
return node;
|
||||
},
|
||||
getElementById() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function loadComponent(document) {
|
||||
const registry = {};
|
||||
vm.runInNewContext(source, {
|
||||
console,
|
||||
Date,
|
||||
Math,
|
||||
document,
|
||||
window: {
|
||||
VerstakPluginRegister(pluginId, bundle) {
|
||||
registry[pluginId] = bundle.components || {};
|
||||
},
|
||||
},
|
||||
}, { filename: sourcePath });
|
||||
const component = registry['verstak.activity'] && registry['verstak.activity'].ActivityView;
|
||||
if (!component) throw new Error('ActivityView was not registered');
|
||||
return component;
|
||||
}
|
||||
|
||||
function makeApi(initialSettings = {}) {
|
||||
const settings = { ...initialSettings };
|
||||
const handlers = {};
|
||||
const unsubscribed = [];
|
||||
return {
|
||||
handlers,
|
||||
unsubscribed,
|
||||
settings: {
|
||||
read: async (key) => (key ? settings[key] : { ...settings }),
|
||||
write: async (key, value) => {
|
||||
settings[key] = value;
|
||||
return { ...settings };
|
||||
},
|
||||
},
|
||||
events: {
|
||||
subscribe: async (name, handler) => {
|
||||
handlers[name] = handler;
|
||||
return () => {
|
||||
unsubscribed.push(name);
|
||||
delete handlers[name];
|
||||
};
|
||||
},
|
||||
},
|
||||
storedEvents() {
|
||||
return settings.events || [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function flush() {
|
||||
for (let i = 0; i < 10; i += 1) await Promise.resolve();
|
||||
}
|
||||
|
||||
async function mountWithApi(api, document = makeDocument()) {
|
||||
const component = loadComponent(document);
|
||||
const container = new FakeNode('div');
|
||||
component.mount(container, { workspaceNode: { name: 'Project' } }, api);
|
||||
await flush();
|
||||
return { component, container, document };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const api = makeApi();
|
||||
const { component, container } = await mountWithApi(api);
|
||||
|
||||
for (const name of ['file.opened', 'file.changed', 'note.saved', 'action.started', 'browser.capture.received', 'case.selected', 'browser.capture.selection']) {
|
||||
if (typeof api.handlers[name] !== 'function') throw new Error(`${name} subscription missing`);
|
||||
}
|
||||
|
||||
await api.handlers['browser.capture.selection']({
|
||||
name: 'browser.capture.selection',
|
||||
pluginId: 'verstak.browser-inbox',
|
||||
timestamp: '2026-06-27T00:00:00Z',
|
||||
payload: {
|
||||
captureId: 'capture-1',
|
||||
kind: 'selection',
|
||||
title: 'Example Article',
|
||||
url: 'https://example.com/article',
|
||||
text: 'Selected text',
|
||||
},
|
||||
});
|
||||
await flush();
|
||||
|
||||
const stored = api.storedEvents();
|
||||
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 (!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 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 (!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');
|
||||
|
||||
component.unmount && component.unmount(container);
|
||||
if (api.unsubscribed.length !== 9) throw new Error(`expected 9 unsubscribers, got ${api.unsubscribed.length}`);
|
||||
|
||||
const persistedApi = makeApi({
|
||||
events: [{
|
||||
activityId: 'persisted-1',
|
||||
type: 'note.saved',
|
||||
title: 'Saved note',
|
||||
summary: 'Notes/Case.md',
|
||||
occurredAt: '2026-06-27T01:00:00Z',
|
||||
sourcePluginId: 'verstak.notes',
|
||||
}],
|
||||
});
|
||||
const persisted = await mountWithApi(persistedApi);
|
||||
if (!persisted.container.textContent.includes('Saved note')) throw new Error('persisted activity was not rendered');
|
||||
|
||||
console.log('activity plugin smoke passed');
|
||||
})().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in New Issue