mirror of
https://github.com/iconify/iconify.git
synced 2024-11-09 23:00:56 +00:00
Create mock API module in core to allow testing API in components
This commit is contained in:
parent
44692efcb5
commit
31ceb48b6e
@ -17,6 +17,7 @@ import { getStorage, addIconSet } from '../storage/storage';
|
||||
import { coreModules } from '../modules';
|
||||
import type { IconifyIconName, IconifyIconSource } from '../icon/name';
|
||||
import { listToIcons } from '../icon/list';
|
||||
import { allowSimpleNames } from '../storage/functions';
|
||||
|
||||
// Empty abort callback for loadIcons()
|
||||
function emptyCallback(): void {
|
||||
@ -267,7 +268,7 @@ const loadIcons: IconifyLoadIcons = (
|
||||
callback?: IconifyIconLoaderCallback
|
||||
): IconifyIconLoaderAbort => {
|
||||
// Clean up and copy icons list
|
||||
const cleanedIcons = listToIcons(icons, true);
|
||||
const cleanedIcons = listToIcons(icons, true, allowSimpleNames());
|
||||
|
||||
// Sort icons by missing/loaded/pending
|
||||
// Pending means icon is either being requsted or is about to be requested
|
||||
|
206
packages/core/src/api/modules/mock.ts
Normal file
206
packages/core/src/api/modules/mock.ts
Normal file
@ -0,0 +1,206 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import type { PendingQueryItem } from '@cyberalien/redundancy';
|
||||
import type { APIQueryParams, IconifyAPIModule } from '../modules';
|
||||
import type { IconifyJSON } from '@iconify/types';
|
||||
|
||||
/**
|
||||
* Callback for API delay
|
||||
*/
|
||||
export type IconifyMockAPIDelayDoneCallback = () => void;
|
||||
export type IconifyMockAPIDelayCallback = (
|
||||
next: IconifyMockAPIDelayDoneCallback
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Fake API result
|
||||
*/
|
||||
export interface IconifyMockAPI {
|
||||
// Request parameters
|
||||
provider: string;
|
||||
prefix: string;
|
||||
|
||||
// If icons list is missing, applies to all requests
|
||||
// If array, applies to any matching icon
|
||||
icons?: string | string[];
|
||||
|
||||
// Response
|
||||
// Number if error should be sent, JSON on success
|
||||
response: number | IconifyJSON;
|
||||
|
||||
// Delay for response: in milliseconds or callback
|
||||
delay?: number | IconifyMockAPIDelayCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake API storage
|
||||
*
|
||||
* [provider][prefix] = list of entries
|
||||
*/
|
||||
export const storage: Record<
|
||||
string,
|
||||
Record<string, IconifyMockAPI[]>
|
||||
> = Object.create(null);
|
||||
|
||||
/**
|
||||
* Set data for mocking API
|
||||
*/
|
||||
export function mockAPIData(data: IconifyMockAPI): void {
|
||||
const provider = data.provider;
|
||||
if (storage[provider] === void 0) {
|
||||
storage[provider] = Object.create(null);
|
||||
}
|
||||
const providerStorage = storage[provider];
|
||||
|
||||
const prefix = data.prefix;
|
||||
if (providerStorage[prefix] === void 0) {
|
||||
providerStorage[prefix] = [];
|
||||
}
|
||||
|
||||
storage[provider][prefix].push(data);
|
||||
}
|
||||
|
||||
interface MockAPIQueryParams extends APIQueryParams {
|
||||
index: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return API module
|
||||
*/
|
||||
export const mockAPIModule: IconifyAPIModule = {
|
||||
/**
|
||||
* Prepare params
|
||||
*/
|
||||
prepare: (provider: string, prefix: string, icons: string[]) => {
|
||||
if (
|
||||
storage[provider] === void 0 ||
|
||||
storage[provider][prefix] === void 0
|
||||
) {
|
||||
// No mock data: bundle all icons in one request that will return 404
|
||||
return [
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
icons,
|
||||
},
|
||||
];
|
||||
}
|
||||
const mockData = storage[provider][prefix];
|
||||
|
||||
// Find catch all entry with error
|
||||
const catchAllIndex = mockData.findIndex(
|
||||
(item) => item.icons === void 0 && typeof item.response !== 'object'
|
||||
);
|
||||
|
||||
// Find all icons
|
||||
const matches: Record<number, string[]> = Object.create(null);
|
||||
const noMatch: string[] = [];
|
||||
icons.forEach((name) => {
|
||||
let index = mockData.findIndex((item) => {
|
||||
if (item.icons === void 0) {
|
||||
const response = item.response;
|
||||
if (typeof response === 'object') {
|
||||
return (
|
||||
(response.icons &&
|
||||
response.icons[name] !== void 0) ||
|
||||
(response.aliases &&
|
||||
response.aliases[name] !== void 0)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const iconsList = item.icons;
|
||||
if (typeof iconsList === 'string') {
|
||||
return iconsList === name;
|
||||
}
|
||||
if (iconsList instanceof Array) {
|
||||
return iconsList.indexOf(name) !== -1;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
index = catchAllIndex;
|
||||
}
|
||||
|
||||
if (index === -1) {
|
||||
// Not found
|
||||
noMatch.push(name);
|
||||
} else {
|
||||
if (matches[index] === void 0) {
|
||||
matches[index] = [];
|
||||
}
|
||||
matches[index].push(name);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort results
|
||||
const results: APIQueryParams[] = [];
|
||||
if (noMatch.length > 0) {
|
||||
results.push({
|
||||
provider,
|
||||
prefix,
|
||||
icons: noMatch,
|
||||
});
|
||||
}
|
||||
Object.keys(matches).forEach((key) => {
|
||||
const index = parseInt(key);
|
||||
results.push({
|
||||
provider,
|
||||
prefix,
|
||||
icons: matches[index],
|
||||
index,
|
||||
} as APIQueryParams);
|
||||
});
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load icons
|
||||
*/
|
||||
send: (host: string, params: APIQueryParams, status: PendingQueryItem) => {
|
||||
const provider = params.provider;
|
||||
const prefix = params.prefix;
|
||||
const index = (params as MockAPIQueryParams).index;
|
||||
|
||||
// Get item
|
||||
if (
|
||||
storage[provider] === void 0 ||
|
||||
storage[provider][prefix] === void 0 ||
|
||||
storage[provider][prefix][index] === void 0
|
||||
) {
|
||||
// No entry
|
||||
status.done(void 0, 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = storage[provider][prefix][index];
|
||||
|
||||
// Get delay
|
||||
const delay = data.delay;
|
||||
let callback: IconifyMockAPIDelayCallback;
|
||||
switch (typeof delay) {
|
||||
case 'function':
|
||||
callback = delay;
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
callback = (next) => setTimeout(next, delay);
|
||||
break;
|
||||
|
||||
default:
|
||||
callback = (next) => next();
|
||||
break;
|
||||
}
|
||||
|
||||
// Run after delay
|
||||
callback(() => {
|
||||
if (typeof data.response === 'number') {
|
||||
status.done(void 0, data.response);
|
||||
} else {
|
||||
status.done(data.response);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
@ -6,16 +6,17 @@ import { stringToIcon, validateIcon } from './name';
|
||||
*/
|
||||
export function listToIcons(
|
||||
list: (string | IconifyIconName)[],
|
||||
validate = true
|
||||
validate = true,
|
||||
simpleNames = false
|
||||
): IconifyIconName[] {
|
||||
const result: IconifyIconName[] = [];
|
||||
|
||||
list.forEach((item) => {
|
||||
const icon: IconifyIconName =
|
||||
typeof item === 'string'
|
||||
? (stringToIcon(item) as IconifyIconName)
|
||||
? (stringToIcon(item, false, simpleNames) as IconifyIconName)
|
||||
: item;
|
||||
if (!validate || validateIcon(icon)) {
|
||||
if (!validate || validateIcon(icon, simpleNames)) {
|
||||
result.push({
|
||||
provider: icon.provider,
|
||||
prefix: icon.prefix,
|
||||
|
@ -68,7 +68,8 @@ export function sortIcons(icons: IconifyIconName[]): SortedIcons {
|
||||
let list;
|
||||
if (localStorage.icons[name] !== void 0) {
|
||||
list = result.loaded;
|
||||
} else if (localStorage.missing[name] !== void 0) {
|
||||
} else if (prefix === '' || localStorage.missing[name] !== void 0) {
|
||||
// Mark icons without prefix as missing because they cannot be loaded from API
|
||||
list = result.missing;
|
||||
} else {
|
||||
list = result.pending;
|
||||
|
159
packages/core/tests/30-api/10-mock-prepare-test.ts
Normal file
159
packages/core/tests/30-api/10-mock-prepare-test.ts
Normal file
@ -0,0 +1,159 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import type { IconifyMockAPI } from '../../lib/api/modules/mock';
|
||||
import {
|
||||
mockAPIModule,
|
||||
mockAPIData,
|
||||
storage,
|
||||
} from '../../lib/api/modules/mock';
|
||||
import type { GetAPIConfig } from '../../lib/api/config';
|
||||
|
||||
describe('Testing mock API module prepare function', () => {
|
||||
let prefixCounter = 0;
|
||||
function nextPrefix(): string {
|
||||
prefixCounter++;
|
||||
return (
|
||||
'api-mock-prepare-' +
|
||||
(prefixCounter < 10 ? '0' : '') +
|
||||
prefixCounter
|
||||
);
|
||||
}
|
||||
|
||||
const prepare = mockAPIModule.prepare;
|
||||
|
||||
it('Setting data for all icons', () => {
|
||||
const provider = nextPrefix();
|
||||
const prefix = nextPrefix();
|
||||
|
||||
const item: IconifyMockAPI = {
|
||||
provider,
|
||||
prefix,
|
||||
response: 404,
|
||||
};
|
||||
mockAPIData(item);
|
||||
|
||||
// Make sure item is stored correctly
|
||||
expect(typeof storage[provider]).to.be.equal('object');
|
||||
expect(storage[provider][prefix]).to.be.eql([item]);
|
||||
|
||||
// Find item for icons
|
||||
const result = prepare(provider, prefix, ['foo', 'bar', 'baz']);
|
||||
expect(result).to.be.eql([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
icons: ['foo', 'bar', 'baz'],
|
||||
index: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Setting multiple entries', () => {
|
||||
const provider = nextPrefix();
|
||||
const prefix = nextPrefix();
|
||||
|
||||
const item1: IconifyMockAPI = {
|
||||
provider,
|
||||
prefix,
|
||||
response: 404,
|
||||
icons: ['foo', 'bar'],
|
||||
};
|
||||
const item2: IconifyMockAPI = {
|
||||
provider,
|
||||
prefix,
|
||||
response: 404,
|
||||
icons: 'baz',
|
||||
};
|
||||
const item3: IconifyMockAPI = {
|
||||
provider,
|
||||
prefix,
|
||||
response: {
|
||||
prefix,
|
||||
icons: {
|
||||
test10: {
|
||||
body: '<g />',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
mockAPIData(item1);
|
||||
mockAPIData(item2);
|
||||
mockAPIData(item3);
|
||||
|
||||
// Make sure item is stored correctly
|
||||
expect(typeof storage[provider]).to.be.equal('object');
|
||||
expect(storage[provider][prefix]).to.be.eql([item1, item2, item3]);
|
||||
|
||||
// Find items for icons
|
||||
const result = prepare(provider, prefix, [
|
||||
'foo',
|
||||
'baz',
|
||||
'bar',
|
||||
'test1',
|
||||
'test10',
|
||||
'test2',
|
||||
]);
|
||||
expect(result).to.be.eql([
|
||||
// Unknown icons first
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
icons: ['test1', 'test2'],
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
icons: ['foo', 'bar'],
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
icons: ['baz'],
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
icons: ['test10'],
|
||||
index: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Without catch-all query', () => {
|
||||
const provider = nextPrefix();
|
||||
const prefix = nextPrefix();
|
||||
|
||||
const item: IconifyMockAPI = {
|
||||
provider,
|
||||
prefix,
|
||||
response: 404,
|
||||
icons: ['foo'],
|
||||
};
|
||||
mockAPIData(item);
|
||||
|
||||
// Make sure item is stored correctly
|
||||
expect(typeof storage[provider]).to.be.equal('object');
|
||||
expect(storage[provider][prefix]).to.be.eql([item]);
|
||||
|
||||
// Find item for icons
|
||||
const result = prepare(provider, prefix, ['foo', 'bar', 'baz']);
|
||||
expect(result).to.be.eql([
|
||||
// Missing icons first
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
icons: ['bar', 'baz'],
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
icons: ['foo'],
|
||||
index: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
241
packages/core/tests/30-api/30-mock-test.ts
Normal file
241
packages/core/tests/30-api/30-mock-test.ts
Normal file
@ -0,0 +1,241 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { setAPIConfig } from '../../lib/api/config';
|
||||
import { setAPIModule } from '../../lib/api/modules';
|
||||
import { API } from '../../lib/api/';
|
||||
import type { IconifyMockAPIDelayDoneCallback } from '../../lib/api/modules/mock';
|
||||
import { mockAPIModule, mockAPIData } from '../../lib/api/modules/mock';
|
||||
import { allowSimpleNames } from '../../lib/storage/functions';
|
||||
|
||||
describe('Testing mock API module', () => {
|
||||
let prefixCounter = 0;
|
||||
function nextPrefix(): string {
|
||||
prefixCounter++;
|
||||
return 'api-mock-' + (prefixCounter < 10 ? '0' : '') + prefixCounter;
|
||||
}
|
||||
|
||||
// Set API module for provider
|
||||
const provider = nextPrefix();
|
||||
setAPIConfig(provider, {
|
||||
resources: ['https://api1.local'],
|
||||
});
|
||||
setAPIModule(provider, mockAPIModule);
|
||||
|
||||
// Tests
|
||||
it('404 response', (done) => {
|
||||
const prefix = nextPrefix();
|
||||
|
||||
mockAPIData({
|
||||
provider,
|
||||
prefix,
|
||||
icons: ['test1', 'test2'],
|
||||
response: 404,
|
||||
});
|
||||
|
||||
let isSync = true;
|
||||
|
||||
API.loadIcons(
|
||||
[
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test1',
|
||||
},
|
||||
],
|
||||
(loaded, missing, pending) => {
|
||||
expect(isSync).to.be.equal(false);
|
||||
expect(loaded).to.be.eql([]);
|
||||
expect(pending).to.be.eql([]);
|
||||
expect(missing).to.be.eql([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test1',
|
||||
},
|
||||
]);
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
isSync = false;
|
||||
});
|
||||
|
||||
it('Load few icons', (done) => {
|
||||
const prefix = nextPrefix();
|
||||
|
||||
mockAPIData({
|
||||
provider,
|
||||
prefix,
|
||||
response: {
|
||||
prefix,
|
||||
icons: {
|
||||
test10: {
|
||||
body: '<g />',
|
||||
},
|
||||
test11: {
|
||||
body: '<g />',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
mockAPIData({
|
||||
provider,
|
||||
prefix,
|
||||
response: {
|
||||
prefix,
|
||||
icons: {
|
||||
test20: {
|
||||
body: '<g />',
|
||||
},
|
||||
test21: {
|
||||
body: '<g />',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let isSync = true;
|
||||
|
||||
API.loadIcons(
|
||||
[
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test10',
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test20',
|
||||
},
|
||||
],
|
||||
(loaded, missing, pending) => {
|
||||
expect(isSync).to.be.equal(false);
|
||||
// All icons should have been loaded because API waits one tick before sending response, during which both queries are processed
|
||||
expect(loaded).to.be.eql([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test10',
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test20',
|
||||
},
|
||||
]);
|
||||
expect(pending).to.be.eql([]);
|
||||
expect(missing).to.be.eql([]);
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
isSync = false;
|
||||
});
|
||||
|
||||
it('Load in batches and testing delay', (done) => {
|
||||
const prefix = nextPrefix();
|
||||
let next: IconifyMockAPIDelayDoneCallback | undefined;
|
||||
|
||||
mockAPIData({
|
||||
provider,
|
||||
prefix,
|
||||
response: {
|
||||
prefix,
|
||||
icons: {
|
||||
test10: {
|
||||
body: '<g />',
|
||||
},
|
||||
test11: {
|
||||
body: '<g />',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
mockAPIData({
|
||||
provider,
|
||||
prefix,
|
||||
response: {
|
||||
prefix,
|
||||
icons: {
|
||||
test20: {
|
||||
body: '<g />',
|
||||
},
|
||||
test21: {
|
||||
body: '<g />',
|
||||
},
|
||||
},
|
||||
},
|
||||
delay: (callback) => {
|
||||
next = callback;
|
||||
},
|
||||
});
|
||||
|
||||
let callbackCounter = 0;
|
||||
|
||||
API.loadIcons(
|
||||
[
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test10',
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test20',
|
||||
},
|
||||
],
|
||||
(loaded, missing, pending) => {
|
||||
callbackCounter++;
|
||||
switch (callbackCounter) {
|
||||
case 1:
|
||||
// First load: only 'test10'
|
||||
expect(loaded).to.be.eql([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test10',
|
||||
},
|
||||
]);
|
||||
expect(pending).to.be.eql([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test20',
|
||||
},
|
||||
]);
|
||||
|
||||
// Send second response
|
||||
expect(typeof next).to.be.equal('function');
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
next!();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// All icons should have been loaded
|
||||
expect(loaded).to.be.eql([
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test10',
|
||||
},
|
||||
{
|
||||
provider,
|
||||
prefix,
|
||||
name: 'test20',
|
||||
},
|
||||
]);
|
||||
expect(missing).to.be.eql([]);
|
||||
done();
|
||||
break;
|
||||
|
||||
default:
|
||||
done('Callback was called more times than expected');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
91
packages/core/tests/30-api/40-simple-names-test.ts
Normal file
91
packages/core/tests/30-api/40-simple-names-test.ts
Normal file
@ -0,0 +1,91 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { setAPIConfig } from '../../lib/api/config';
|
||||
import { setAPIModule } from '../../lib/api/modules';
|
||||
import { API } from '../../lib/api/';
|
||||
import { mockAPIModule, mockAPIData } from '../../lib/api/modules/mock';
|
||||
import { allowSimpleNames } from '../../lib/storage/functions';
|
||||
|
||||
describe('Testing simple names with API module', () => {
|
||||
// Set API config and allow simple names
|
||||
before(() => {
|
||||
setAPIConfig('', {
|
||||
resources: ['https://api1.local'],
|
||||
});
|
||||
allowSimpleNames(true);
|
||||
setAPIModule('', mockAPIModule);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
allowSimpleNames(false);
|
||||
});
|
||||
|
||||
it('Loading icons without prefix', (done) => {
|
||||
mockAPIData({
|
||||
provider: '',
|
||||
prefix: '',
|
||||
response: {
|
||||
prefix: '',
|
||||
icons: {
|
||||
test100: {
|
||||
body: '<g />',
|
||||
},
|
||||
test101: {
|
||||
body: '<g />',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
mockAPIData({
|
||||
provider: '',
|
||||
prefix: 'test200',
|
||||
response: {
|
||||
prefix: 'test200',
|
||||
icons: {
|
||||
foo: {
|
||||
body: '<g />',
|
||||
},
|
||||
bar: {
|
||||
body: '<g />',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
API.loadIcons(
|
||||
[
|
||||
{
|
||||
provider: '',
|
||||
prefix: '',
|
||||
name: 'test100',
|
||||
},
|
||||
{
|
||||
provider: '',
|
||||
prefix: 'test200',
|
||||
name: 'foo',
|
||||
},
|
||||
],
|
||||
(loaded, missing, pending) => {
|
||||
// 'test100' should be missing because it does not have a prefix
|
||||
expect(loaded).to.be.eql([
|
||||
{
|
||||
provider: '',
|
||||
prefix: 'test200',
|
||||
name: 'foo',
|
||||
},
|
||||
]);
|
||||
expect(pending).to.be.eql([]);
|
||||
expect(missing).to.be.eql([
|
||||
{
|
||||
provider: '',
|
||||
prefix: '',
|
||||
name: 'test100',
|
||||
},
|
||||
]);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user