feat: plugins v1
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { Injector } from '@angular/core';
|
||||
import type { TojuPluginManifest } from '../../../../shared-kernel';
|
||||
import { PluginStoreService } from './plugin-store.service';
|
||||
import { PluginHostService } from './plugin-host.service';
|
||||
import { PluginRegistryService } from './plugin-registry.service';
|
||||
import type { PluginStoreEntry } from '../../domain/models/plugin-store.models';
|
||||
|
||||
describe('PluginStoreService', () => {
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
let registerLocalManifest: ReturnType<typeof vi.fn>;
|
||||
let unregister: ReturnType<typeof vi.fn>;
|
||||
let storage: Storage;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = createMemoryStorage();
|
||||
vi.stubGlobal('localStorage', storage);
|
||||
fetchMock = vi.fn();
|
||||
registerLocalManifest = vi.fn((manifest: TojuPluginManifest, sourcePath?: string) => ({
|
||||
enabled: true,
|
||||
manifest,
|
||||
sourcePath,
|
||||
state: 'validated',
|
||||
validationIssues: []
|
||||
}));
|
||||
|
||||
unregister = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
storage.clear();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('loads plugin entries from source manifests and resolves relative links', async () => {
|
||||
fetchMock.mockResolvedValueOnce(jsonResponse({
|
||||
plugins: [
|
||||
{
|
||||
author: 'Ada Example',
|
||||
description: 'Adds better channel tools.',
|
||||
github: 'https://github.com/example/better-channels',
|
||||
id: 'example.better-channels',
|
||||
image: './images/better.png',
|
||||
install: './better/toju-plugin.json',
|
||||
readme: './better/README.md',
|
||||
title: 'Better Channels',
|
||||
version: '1.2.0'
|
||||
}
|
||||
],
|
||||
title: 'Example Plugins'
|
||||
}));
|
||||
|
||||
const service = createService(registerLocalManifest, unregister);
|
||||
|
||||
await service.addSourceUrl('https://plugins.example.test/index.json#latest');
|
||||
|
||||
expect(service.sourceUrls()).toEqual(['https://plugins.example.test/index.json']);
|
||||
expect(service.sources()[0]?.title).toBe('Example Plugins');
|
||||
expect(service.availablePlugins()).toEqual([
|
||||
expect.objectContaining({
|
||||
author: 'Ada Example',
|
||||
githubUrl: 'https://github.com/example/better-channels',
|
||||
id: 'example.better-channels',
|
||||
imageUrl: 'https://plugins.example.test/images/better.png',
|
||||
installUrl: 'https://plugins.example.test/better/toju-plugin.json',
|
||||
readmeUrl: 'https://plugins.example.test/better/README.md',
|
||||
sourceTitle: 'Example Plugins',
|
||||
title: 'Better Channels',
|
||||
version: '1.2.0'
|
||||
})
|
||||
]);
|
||||
});
|
||||
|
||||
it('installs, detects updates, and uninstalls store plugins', async () => {
|
||||
const manifest = createManifest({ version: '1.0.0' });
|
||||
const plugin = createStoreEntry({ version: '1.0.0' });
|
||||
|
||||
fetchMock.mockResolvedValueOnce(jsonResponse(manifest));
|
||||
|
||||
const service = createService(registerLocalManifest, unregister);
|
||||
|
||||
await service.installPlugin(plugin);
|
||||
|
||||
expect(registerLocalManifest).toHaveBeenCalledWith(manifest, plugin.installUrl);
|
||||
expect(service.installedPlugins()[0]?.manifest.id).toBe(plugin.id);
|
||||
expect(service.getActionLabel(plugin)).toBe('Uninstall');
|
||||
expect(service.getActionLabel(createStoreEntry({ version: '1.1.0' }))).toBe('Update');
|
||||
|
||||
service.uninstallPlugin(plugin.id);
|
||||
|
||||
expect(unregister).toHaveBeenCalledWith(plugin.id);
|
||||
expect(service.installedPlugins()).toEqual([]);
|
||||
});
|
||||
|
||||
it('loads plugin readmes as markdown text', async () => {
|
||||
const plugin = createStoreEntry({ readmeUrl: 'https://plugins.example.test/better/README.md' });
|
||||
|
||||
fetchMock.mockResolvedValueOnce(textResponse('# Better Channels'));
|
||||
|
||||
const service = createService(registerLocalManifest, unregister);
|
||||
const readme = await service.loadReadme(plugin);
|
||||
|
||||
expect(readme).toEqual({
|
||||
markdown: '# Better Channels',
|
||||
pluginId: plugin.id,
|
||||
title: plugin.title,
|
||||
url: plugin.readmeUrl
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createService(
|
||||
registerLocalManifest: ReturnType<typeof vi.fn>,
|
||||
unregister: ReturnType<typeof vi.fn>
|
||||
): PluginStoreService {
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
PluginStoreService,
|
||||
{
|
||||
provide: PluginHostService,
|
||||
useValue: { registerLocalManifest }
|
||||
},
|
||||
{
|
||||
provide: PluginRegistryService,
|
||||
useValue: { unregister }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return injector.get(PluginStoreService);
|
||||
}
|
||||
|
||||
function createManifest(overrides: Partial<TojuPluginManifest> = {}): TojuPluginManifest {
|
||||
return {
|
||||
apiVersion: '1.0.0',
|
||||
compatibility: {
|
||||
minimumTojuVersion: '1.0.0'
|
||||
},
|
||||
description: 'Adds better channel tools.',
|
||||
entrypoint: './dist/main.js',
|
||||
id: 'example.better-channels',
|
||||
kind: 'client',
|
||||
schemaVersion: 1,
|
||||
title: 'Better Channels',
|
||||
version: '1.0.0',
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function createStoreEntry(overrides: Partial<PluginStoreEntry> = {}): PluginStoreEntry {
|
||||
return {
|
||||
author: 'Ada Example',
|
||||
description: 'Adds better channel tools.',
|
||||
githubUrl: 'https://github.com/example/better-channels',
|
||||
id: 'example.better-channels',
|
||||
imageUrl: 'https://plugins.example.test/images/better.png',
|
||||
installUrl: 'https://plugins.example.test/better/toju-plugin.json',
|
||||
readmeUrl: 'https://plugins.example.test/better/README.md',
|
||||
sourceTitle: 'Example Plugins',
|
||||
sourceUrl: 'https://plugins.example.test/index.json',
|
||||
title: 'Better Channels',
|
||||
version: '1.0.0',
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function jsonResponse(value: unknown): Response {
|
||||
return {
|
||||
json: vi.fn(async () => value),
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: vi.fn(async () => JSON.stringify(value))
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
function textResponse(value: string): Response {
|
||||
return {
|
||||
json: vi.fn(async () => JSON.parse(value) as unknown),
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: vi.fn(async () => value)
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
function createMemoryStorage(): Storage {
|
||||
const values = new Map<string, string>();
|
||||
|
||||
return {
|
||||
get length(): number {
|
||||
return values.size;
|
||||
},
|
||||
clear: vi.fn(() => values.clear()),
|
||||
getItem: vi.fn((key: string) => values.get(key) ?? null),
|
||||
key: vi.fn((index: number) => Array.from(values.keys())[index] ?? null),
|
||||
removeItem: vi.fn((key: string) => values.delete(key)),
|
||||
setItem: vi.fn((key: string, value: string) => values.set(key, value))
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user