Packages added: - @bytelyst/referral-client — referral API client + share helpers - @bytelyst/subscription-client — subscription/plan API client + cache - @bytelyst/celebrations — milestone triggers, confetti, positive messages - @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases - @bytelyst/accessibility — VoiceOver/TalkBack label generators - @bytelyst/quick-actions — progressive disclosure, smart defaults - @bytelyst/time-references — familiar duration references - @bytelyst/org-client — org/workspace/membership/license API client - @bytelyst/marketplace-client — listing/review/install API client All packages: pure TS, ESM, globalThis.fetch, no Node.js deps. 99 Vitest tests across 9 packages, 79/79 public methods covered. Review fixes applied: - time-references: fix module-level mutable state leak + add clearCustomReferences() - accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords - subscription-client: fix flaky daysRemaining test (ms boundary race)
283 lines
8.0 KiB
TypeScript
283 lines
8.0 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { createMarketplaceClient } from './client.js';
|
|
import type {
|
|
MarketplaceListingDoc,
|
|
MarketplaceReviewDoc,
|
|
MarketplaceInstallDoc,
|
|
} from './types.js';
|
|
|
|
const baseConfig = {
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
getAccessToken: () => 'test-token',
|
|
};
|
|
|
|
function mockListing(overrides?: Partial<MarketplaceListingDoc>): MarketplaceListingDoc {
|
|
return {
|
|
id: 'lst_1',
|
|
productId: 'testapp',
|
|
templateType: 'fasting_protocol',
|
|
authorId: 'user-1',
|
|
authorName: 'Alice',
|
|
title: '16:8 Protocol',
|
|
shortDescription: 'Classic intermittent fasting',
|
|
description: 'A detailed 16:8 fasting protocol.',
|
|
tags: ['fasting', 'beginner'],
|
|
category: 'protocols',
|
|
payload: { hours: 16, breakHours: 8 },
|
|
pricingModel: 'free',
|
|
priceInCents: 0,
|
|
certificationStatus: 'approved',
|
|
installCount: 100,
|
|
reviewCount: 10,
|
|
averageRating: 4.5,
|
|
visibility: 'public',
|
|
featured: false,
|
|
version: '1.0.0',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mockReview(overrides?: Partial<MarketplaceReviewDoc>): MarketplaceReviewDoc {
|
|
return {
|
|
id: 'rev_1',
|
|
listingId: 'lst_1',
|
|
productId: 'testapp',
|
|
authorId: 'user-2',
|
|
rating: 5,
|
|
title: 'Great protocol!',
|
|
body: 'Really works well for me.',
|
|
verified: true,
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mockInstall(overrides?: Partial<MarketplaceInstallDoc>): MarketplaceInstallDoc {
|
|
return {
|
|
id: 'inst_1',
|
|
listingId: 'lst_1',
|
|
productId: 'testapp',
|
|
userId: 'user-2',
|
|
version: '1.0.0',
|
|
installedAt: '2026-01-01T00:00:00Z',
|
|
uninstalledAt: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('createMarketplaceClient', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should list listings', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ listings: [mockListing()], total: 1 }),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.listListings({ category: 'protocols' });
|
|
expect(result.listings).toHaveLength(1);
|
|
expect(result.total).toBe(1);
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
const url = fetchMock.mock.calls[0][0] as string;
|
|
expect(url).toContain('category=protocols');
|
|
});
|
|
|
|
it('should get a listing by id', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockListing()),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.getListing('lst_1');
|
|
expect(result.title).toBe('16:8 Protocol');
|
|
});
|
|
|
|
it('should create a listing', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockListing()),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.createListing({
|
|
templateType: 'fasting_protocol',
|
|
title: '16:8 Protocol',
|
|
shortDescription: 'Classic',
|
|
description: 'Detailed',
|
|
category: 'protocols',
|
|
payload: { hours: 16 },
|
|
});
|
|
expect(result.id).toBe('lst_1');
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
expect.stringContaining('/marketplace/listings'),
|
|
expect.objectContaining({ method: 'POST' })
|
|
);
|
|
});
|
|
|
|
it('should submit for certification', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockListing({ certificationStatus: 'submitted' })),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.submitForCertification('lst_1', 'Ready for review');
|
|
expect(result.certificationStatus).toBe('submitted');
|
|
});
|
|
|
|
it('should install a listing', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockInstall()),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.installListing('lst_1');
|
|
expect(result.listingId).toBe('lst_1');
|
|
});
|
|
|
|
it('should list my installs', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve([mockInstall()]),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.listMyInstalls();
|
|
expect(result).toHaveLength(1);
|
|
});
|
|
|
|
it('should list reviews', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve([mockReview()]),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.listReviews('lst_1');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].rating).toBe(5);
|
|
});
|
|
|
|
it('should create a review', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockReview()),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.createReview('lst_1', {
|
|
rating: 5,
|
|
title: 'Great!',
|
|
body: 'Love it.',
|
|
});
|
|
expect(result.rating).toBe(5);
|
|
});
|
|
|
|
it('should update a listing', async () => {
|
|
const updated = mockListing({ title: 'Updated Title' });
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(updated),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
const result = await client.updateListing('lst_1', { title: 'Updated Title' });
|
|
expect(result.title).toBe('Updated Title');
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/marketplace/listings/lst_1',
|
|
expect.objectContaining({ method: 'PATCH' })
|
|
);
|
|
});
|
|
|
|
it('should uninstall a listing', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
await expect(client.uninstallListing('lst_1')).resolves.toBeUndefined();
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/marketplace/listings/lst_1/uninstall',
|
|
expect.objectContaining({ method: 'POST' })
|
|
);
|
|
});
|
|
|
|
it('should report a listing', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
await expect(
|
|
client.reportListing('lst_1', { reason: 'spam', details: 'Looks like spam' })
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should throw on non-ok response', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
await expect(client.getListing('lst_1')).rejects.toThrow('getListing failed: 500');
|
|
});
|
|
|
|
it('should send correct headers', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ listings: [], total: 0 }),
|
|
})
|
|
);
|
|
const client = createMarketplaceClient(baseConfig);
|
|
await client.listListings();
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
const callHeaders = fetchMock.mock.calls[0][1].headers as Record<string, string>;
|
|
expect(callHeaders['x-product-id']).toBe('testapp');
|
|
expect(callHeaders['Authorization']).toBe('Bearer test-token');
|
|
expect(callHeaders['x-request-id']).toBeDefined();
|
|
});
|
|
});
|