2
0
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:
Vjacheslav Trushkin 2021-04-25 11:21:59 +03:00
parent 44692efcb5
commit 31ceb48b6e
7 changed files with 705 additions and 5 deletions

View File

@ -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

View 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);
}
});
},
};

View File

@ -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,

View File

@ -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;

View 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,
},
]);
});
});

View 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');
}
}
);
});
});

View 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();
}
);
});
});