2
0
mirror of https://github.com/iconify/iconify.git synced 2024-12-05 02:33:16 +00:00

Rewrite SVG framework with new unit tests, better attribute watching, cleaner render

This commit is contained in:
Vjacheslav Trushkin 2022-04-07 15:52:57 +03:00
parent 70ed79a516
commit 7700ea1808
67 changed files with 3449 additions and 8599 deletions

View File

@ -64,7 +64,6 @@ Other packages:
- [Sapper demo](./demo/sapper-demo/) - demo for Sapper, using Svelte component on the server and in the browser. Run `npm run dev` to start the demo (deprecated, use SvelteKit instead of Sapper).
- [SvelteKit demo](./demo/sveltekit-demo/) - demo for SvelteKit, using Svelte component on the server and in the browser. Run `npm run dev` to start the demo.
- [Ember demo](./demo/ember-demo/) - demo for Ember component. Run `npm run start` to start demo.
- [Browser tests](./demo/browser-tests/) - unit tests for SVG framework. Run `npm run build` to build it. Open test.html in browser (requires HTTP server).
## Installation
@ -107,7 +106,7 @@ This monorepo uses symbolic links to create links between packages. This allows
When using Windows, symbolic links require setting up extra permissions. If you are using Windows and cannot set permissions for symbolic links, there are several options:
- Use Windows Subsystem for Linux (WSL).
- Treat each package as a separate package, without links to other packages. All packages do have correct dependencies, so you will be able to use most packages (except for `browser-tests` that requires links to access directory `lib` from `iconify` package), but you will not be able to work on multiple packages at the same time.
- Treat each package as a separate package, without links to other packages. All packages do have correct dependencies, so you will be able to use most packages, but you will not be able to work on multiple packages at the same time.
## Documentation

View File

@ -1,4 +0,0 @@
.DS_Store
node_modules
lib
dist

View File

@ -1,145 +0,0 @@
const fs = require('fs');
const { dirname } = require('path');
const child_process = require('child_process');
const testsDir = __dirname;
const librariesDir = dirname(dirname(testsDir)) + '/packages';
// List of commands to run
const commands = [];
// Parse command line
const compile = {
core: false,
iconify: false,
lib: true,
dist: true,
};
process.argv.slice(2).forEach((cmd) => {
if (cmd.slice(0, 2) !== '--') {
return;
}
const parts = cmd.slice(2).split('-');
if (parts.length === 2) {
// Parse 2 part commands like --with-lib
const key = parts.pop();
if (compile[key] === void 0) {
return;
}
switch (parts.shift()) {
case 'with':
// enable module
compile[key] = true;
break;
case 'without':
// disable module
compile[key] = false;
break;
case 'only':
// disable other modules
Object.keys(compile).forEach((key2) => {
compile[key2] = key2 === key;
});
break;
}
}
});
// Check if required modules in same monorepo are available
const fileExists = (file) => {
try {
fs.statSync(file);
} catch (e) {
return false;
}
return true;
};
if (compile.dist && !fileExists(testsDir + '/lib/node.js')) {
compile.lib = true;
}
if (
compile.lib &&
(!fileExists(librariesDir + '/iconify/dist/iconify.js') ||
!fileExists(librariesDir + '/iconify/lib/iconify.js'))
) {
compile.iconify = true;
}
if (compile.iconify && !fileExists(librariesDir + '/core/lib/modules.mjs')) {
compile.core = true;
}
// Compile core before compiling this package
if (compile.core) {
commands.push({
cmd: 'npm',
args: ['run', 'build'],
cwd: librariesDir + '/core',
});
}
if (compile.iconify || compile.core) {
commands.push({
cmd: 'npm',
args: ['run', 'build'],
cwd: librariesDir + '/iconify',
});
}
// Compile other packages
Object.keys(compile).forEach((key) => {
if (key !== 'core' && key !== 'iconify' && compile[key]) {
commands.push({
cmd: 'npm',
args: ['run', 'build:' + key],
});
}
});
/**
* Run next command
*/
const next = () => {
const item = commands.shift();
if (item === void 0) {
process.exit(0);
}
if (item.cwd === void 0) {
item.cwd = __dirname;
}
const result = child_process.spawnSync(item.cmd, item.args, {
cwd: item.cwd,
stdio: 'inherit',
});
if (result.status === 0) {
process.nextTick(next);
} else {
process.exit(result.status);
}
};
next();
// Update version number in package.json
const packageJSON = JSON.parse(
fs.readFileSync(testsDir + '/package.json', 'utf8')
);
let iconifyVersion = packageJSON.devDependencies['@iconify/iconify'].replace(
/[\^~]/g,
''
);
if (packageJSON.version !== iconifyVersion) {
console.log('Updated package version to', iconifyVersion);
packageJSON.version = iconifyVersion;
fs.writeFileSync(
testsDir + '/package.json',
JSON.stringify(packageJSON, null, '\t') + '\n',
'utf8'
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
{
"name": "@iconify-demo/browser-tests",
"private": true,
"description": "Browser tests for @iconify/iconify package",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "2.2.1",
"license": "(Apache-2.0 OR GPL-2.0)",
"bugs": "https://github.com/iconify/iconify/issues",
"homepage": "https://iconify.design/",
"repository": {
"type": "git",
"url": "https://github.com/iconify/iconify.git",
"directory": "packages/browser-tests"
},
"scripts": {
"build": "node build",
"build:lib": "tsc -b",
"build:dist": "rollup -c rollup.config.js"
},
"devDependencies": {
"@iconify/iconify": "^2.2.1",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^10.0.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.1.0",
"chai": "^4.3.4",
"mocha": "^9.2.0",
"rollup": "^2.66.0",
"typescript": "^4.6.3"
}
}

View File

@ -1,61 +0,0 @@
import fs from 'fs';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
const match = '-test.ts';
// Find files
let files = fs
.readdirSync('tests')
.sort()
.filter((file) => file.slice(0 - match.length) === match);
// Remove suffix
files = files.map((file) => file.slice(0, file.length - match.length));
// Debug one test
// files = ['21-scan-dom-api'];
// Get config files
const tests = [];
const config = files.map((file) => {
tests.push(file + '.js');
return {
input: 'lib/' + file + match.replace('.ts', '.js'),
output: {
file: 'dist/' + file + '.js',
format: 'iife',
globals: {
mocha: 'mocha',
chai: 'chai',
},
},
external: ['mocha', 'chai'],
plugins: [
resolve({
// browser: true,
// exxtensions: ['.js'],
}),
commonjs({
ignore: ['cross-fetch'],
}),
],
};
});
// Write tests.html
let content = fs.readFileSync(__dirname + '/tests/tests.html', 'utf8');
content = content.replace(
'<!-- tests -->',
tests
.map((file) => {
return '<script src="./' + file + '"></script>';
})
.join('')
);
try {
fs.mkdirSync(__dirname + '/dist', 0o755);
} catch (err) {}
fs.writeFileSync(__dirname + '/dist/tests.html', content, 'utf8');
export default config;

View File

@ -1,201 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { FakeData, setFakeData, prepareQuery, sendQuery } from './fake-api';
import { setAPIModule } from '@iconify/core/lib/api/modules';
import { addAPIProvider } from '@iconify/core/lib/api/config';
import { loadIcons } from '@iconify/core/lib/api/icons';
const expect = chai.expect;
let prefixCounter = 0;
function nextPrefix(): string {
return 'fake-api-' + prefixCounter++;
}
describe('Testing fake API', () => {
before(() => {
setAPIModule('', {
prepare: prepareQuery,
send: sendQuery,
});
});
it('Loading results', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1', 'icon2'],
data: {
prefix,
icons: {
icon1: {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
icon2: {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
addAPIProvider(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
setFakeData(provider, prefix, data);
// Attempt to load icons
loadIcons(
[
provider + ':' + prefix + ':icon1',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending) => {
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
]);
done();
}
);
});
it('Loading results with delay', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1', 'icon2'],
delay: 100,
data: {
prefix,
icons: {
icon1: {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
icon2: {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
addAPIProvider(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
setFakeData(provider, prefix, data);
// Attempt to load icons
const start = Date.now();
loadIcons(
[
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
],
(loaded, missing, pending) => {
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
{
provider,
prefix,
name: 'icon2',
},
]);
const end = Date.now();
expect(end - start).to.be.at.least(50);
expect(end - start).to.be.at.most(150);
done();
}
);
});
it('Loading partial results', (done) => {
const provider = nextPrefix();
const prefix = nextPrefix();
const data: FakeData = {
icons: ['icon1'],
delay: 20,
data: {
prefix,
icons: {
icon1: {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
addAPIProvider(provider, {
resources: ['https://api1.local', 'https://api2.local'],
rotate: 20,
timeout: 100,
});
setFakeData(provider, prefix, data);
// Attempt to load icons
let counter = 0;
loadIcons(
[
provider + ':' + prefix + ':icon1',
provider + ':' + prefix + ':icon2',
],
(loaded, missing, pending) => {
try {
counter++;
switch (counter) {
case 1:
// Loaded icon1
expect(loaded).to.be.eql([
{
provider,
prefix,
name: 'icon1',
},
]);
expect(pending).to.be.eql([
{
provider,
prefix,
name: 'icon2',
},
]);
expect(missing).to.be.eql([]);
done();
break;
case 2:
done(
'Callback should not be called ' +
counter +
' times.'
);
}
} catch (err) {
done(err);
}
}
);
});
});

View File

@ -1,347 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import { finder } from '@iconify/iconify/lib/finders/iconify';
import { IconifyElement } from '@iconify/iconify/lib/modules/element';
import { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
const expect = chai.expect;
describe('Testing Iconify finder', () => {
it('Finding nodes and getting node name', () => {
const node = getNode('iconify-finder');
node.innerHTML =
'<div><p>Testing <span>icons</span> placeholders (not replaced with SVG)</p><ul>' +
'<li>Valid icons: <span class="iconify" data-icon="mdi:home"></span><i class="iconify-inline" data-icon="mdi:account"></i></li>' +
'<li>Icon without name: <span class="iconify"></span></li>' +
'<li>Icon with extra classes: <i class="iconify iconify--mdi" data-icon="mdi:home"></i></li>' +
'<li>Icon within icon: <span class="iconify" data-icon="mdi:alert:invalid"><i class="iconify" data-icon="mdi:question">text</i></span></li>' +
'<li>Icon with wrong tag: <p class="iconify" data-icon="mdi:alert"></p></li>' +
'</ul></div>';
// Get icons, convert to array
const results = finder.find(node);
const list: Element[] = Array.prototype.slice.call(results, 0);
// Test valid icons
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(finder.name(element as IconifyElement)).to.be.equal('mdi:home');
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('I');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:account');
expect(finder.name(element as IconifyElement)).to.be.equal(
'mdi:account'
);
// Icon without name
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal(null);
expect(finder.name(element as IconifyElement)).to.be.equal(null);
// Icon with extra classes
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('I');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
expect(finder.name(element as IconifyElement)).to.be.equal('mdi:home');
// Icon within icon
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal(
'mdi:alert:invalid'
);
expect(finder.name(element as IconifyElement)).to.be.equal(
'mdi:alert:invalid' // Validation is done in finder.ts, not in finder instance
);
element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('I');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:question');
expect(finder.name(element as IconifyElement)).to.be.equal(
'mdi:question'
);
// No more icons
element = list.shift();
expect(element).to.be.equal(void 0);
});
it('Transformations and inline/block', () => {
const node = getNode('iconify-finder');
node.innerHTML =
'This test does not render SVG!<br />' +
'Block icons:' +
' <span class="iconify-inline" data-icon="mdi:home" data-inline="false"></span>' +
'Inline rotated icons:' +
' <span class="iconify-inline" data-icon="mdi:account" data-rotate="90deg"></span>' +
' <span class="iconify iconify-inline" data-icon="mdi:account-circle" data-rotate="2"></span>' +
'Block rotated icons:' +
' <span class="iconify" data-icon="mdi:account-box" data-rotate="175%"></span>' +
// Invalid rotation
' <span class="iconify" data-icon="mdi:user" data-rotate="30%"></span>' +
'Flip:' +
' <span class="iconify" data-icon="ic:baseline-account" data-flip="horizontal,vertical"></span>' +
// Double 'horizontal' flip: second entry should not change anything
' <span class="iconify" data-icon="ic:twotone-account" data-flip="horizontal,vertical,horizontal"></span>' +
' <span class="iconify" data-icon="ic:round-account" data-hFlip="true"></span>' +
' <span class="iconify" data-icon="ic:sharp-account" data-vFlip="true"></span>' +
' <span class="iconify" data-icon="ic:box-account" data-vFlip="false"></span>' +
// Invalid value
' <span class="iconify" data-icon="ic:outline-account" data-hFlip="invalid"></span>' +
'';
// Get icons, convert to array
const results = finder.find(node);
const list: Element[] = Array.prototype.slice.call(results, 0);
function testElement(
name: string,
expected: IconifyIconCustomisations
): void {
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.getAttribute('data-icon')).to.be.equal(name);
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
}
// Block icon
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
const expected: IconifyIconCustomisations = {
inline: false,
};
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
// Rotated icons
testElement('mdi:account', {
inline: true,
rotate: 1,
});
testElement('mdi:account-circle', {
inline: true,
rotate: 2,
});
testElement('mdi:account-box', {
inline: false,
rotate: 3,
});
testElement('mdi:user', {
inline: false,
// No rotation because 30% is not a valid value
});
// Flip
testElement('ic:baseline-account', {
inline: false,
hFlip: true,
vFlip: true,
});
testElement('ic:twotone-account', {
inline: false,
hFlip: true,
vFlip: true,
});
testElement('ic:round-account', {
inline: false,
hFlip: true,
});
testElement('ic:sharp-account', {
inline: false,
vFlip: true,
});
testElement('ic:box-account', {
inline: false,
vFlip: false,
});
testElement('ic:outline-account', {
inline: false,
});
// No more icons
element = list.shift();
expect(element).to.be.equal(void 0);
});
it('Dimensions', () => {
const node = getNode('iconify-finder');
node.innerHTML =
'This test does not render SVG!<br />' +
'Block icon:' +
' <span class="iconify iconify-inline" data-icon="mdi:home" data-inline="false"></span>' +
'Width and height:' +
' <span class="iconify" data-icon="mdi:account" data-width="24" data-height="24"></span>' +
' <span class="iconify" data-icon="mdi:account-box" data-width="100%" data-height="100%"></span>' +
'Width:' +
' <span class="iconify" data-icon="mdi:account-twotone" data-width="32" data-inline="true"></span>' +
' <span class="iconify" data-icon="mdi:account-outline" data-width="auto" data-height=""></span>' +
'Height:' +
' <span class="iconify-inline" data-icon="mdi:account-sharp" data-height="2em" data-inline="false"></span>' +
' <span class="iconify-inline" data-icon="mdi:account-square" data-height="auto" data-width=""></span>' +
'';
// Get icons, convert to array
const results = finder.find(node);
const list: Element[] = Array.prototype.slice.call(results, 0);
function testElement(
name: string,
expected: IconifyIconCustomisations
): void {
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.getAttribute('data-icon')).to.be.equal(name);
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
}
// Block icon
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('SPAN');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
const expected: IconifyIconCustomisations = {
inline: false,
};
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
// Width and height
testElement('mdi:account', {
inline: false,
width: '24',
height: '24',
});
testElement('mdi:account-box', {
inline: false,
width: '100%',
height: '100%',
});
// Width only
testElement('mdi:account-twotone', {
inline: true,
width: '32',
});
testElement('mdi:account-outline', {
inline: false,
width: 'auto',
});
// Height only
testElement('mdi:account-sharp', {
inline: false,
height: '2em',
});
testElement('mdi:account-square', {
inline: true,
height: 'auto',
});
// No more icons
element = list.shift();
expect(element).to.be.equal(void 0);
});
it('Alignment', () => {
const node = getNode('iconify-finder');
node.innerHTML =
'This test does not render SVG!<br />' +
'Inline icon:' +
' <i class="iconify" data-icon="mdi:home" data-inline="true"></i>' +
'Alignment:' +
' <i class="iconify" data-icon="mdi:account" data-align="left,top"></i>' +
' <i class="iconify" data-icon="mdi:account-box" data-align="right,slice"></i>' +
// 'bottom' overrides 'top', 'center' overrides 'right', extra comma
' <i class="iconify-inline" data-icon="mdi:account-outline" data-align="top,right,bottom,meet,center,"></i>' +
// spaces instead of comma, 'middle' overrides 'bottom'
' <i class="iconify iconify-inline" data-icon="mdi:account-twotone" data-align="bottom middle"></i>' +
'';
// Get icons, convert to array
const results = finder.find(node);
const list: Element[] = Array.prototype.slice.call(results, 0);
function testElement(
name: string,
expected: IconifyIconCustomisations
): void {
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.getAttribute('data-icon')).to.be.equal(name);
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
}
// First icon
let element = list.shift();
expect(element).to.not.be.equal(void 0);
expect(element.tagName).to.be.equal('I');
expect(element.getAttribute('data-icon')).to.be.equal('mdi:home');
const expected: IconifyIconCustomisations = {
inline: true,
};
expect(finder.customisations(element as IconifyElement)).to.be.eql(
expected
);
// Alignment
testElement('mdi:account', {
inline: false,
hAlign: 'left',
vAlign: 'top',
});
testElement('mdi:account-box', {
inline: false,
hAlign: 'right',
slice: true,
});
testElement('mdi:account-outline', {
inline: true,
hAlign: 'center',
vAlign: 'bottom',
slice: false,
});
testElement('mdi:account-twotone', {
inline: true,
vAlign: 'middle',
});
// No more icons
element = list.shift();
expect(element).to.be.equal(void 0);
});
});

View File

@ -1,82 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import {
addFinder,
findPlaceholders,
} from '@iconify/iconify/lib/modules/finder';
import { IconifyFinder } from '@iconify/iconify/lib/finders/interface';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify-v1';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-v1-icon';
import { IconifyIconName } from '@iconify/utils/lib/icon/name';
const expect = chai.expect;
describe('Testing legacy finder', () => {
before(() => {
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
});
it('Finding nodes', () => {
const node = getNode('finder');
node.innerHTML =
'<div><p>List of <span>icon</span> placeholders (this test does not render SVG)</p><ul>' +
'<li>Valid icons:' +
' <span class="iconify" data-icon="mdi:home"></span>' +
' <i class="iconify" data-icon="mdi:account"></i>' +
'</li>' +
'<li>Icon without name:' +
' <span class="iconify"></span>' +
'</li>' +
'<li>Block icon:' +
' <iconify-icon data-icon="ic:baseline-account"></iconify-icon>' +
'</li>' +
'<li>Icon with wrong tag: <p class="iconify" data-icon="mdi:alert"></p></li>' +
'</ul></div>';
const items = findPlaceholders(node);
function testIcon(
name: IconifyIconName | null,
expectedFinder: IconifyFinder
): void {
const item = items.shift();
expect(item.name).to.be.eql(name);
expect(item.finder).to.be.equal(expectedFinder);
}
// Test all icons
testIcon(
{
provider: '',
prefix: 'mdi',
name: 'home',
},
iconifyFinder
);
testIcon(
{
provider: '',
prefix: 'mdi',
name: 'account',
},
iconifyFinder
);
testIcon(
{
provider: '',
prefix: 'ic',
name: 'baseline-account',
},
iconifyIconFinder
);
// End of list
expect(items.shift()).to.be.equal(void 0);
});
});

View File

@ -1,82 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import {
addFinder,
findPlaceholders,
} from '@iconify/iconify/lib/modules/finder';
import { IconifyFinder } from '@iconify/iconify/lib/finders/interface';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { IconifyIconName } from '@iconify/utils/lib/icon/name';
const expect = chai.expect;
describe('Testing finder', () => {
before(() => {
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
});
it('Finding nodes', () => {
const node = getNode('finder');
node.innerHTML =
'<div><p>List of <span>icon</span> placeholders (this test does not render SVG)</p><ul>' +
'<li>Valid icons:' +
' <span class="iconify" data-icon="mdi:home"></span>' +
' <i class="iconify" data-icon="mdi:account"></i>' +
'</li>' +
'<li>Icon without name:' +
' <span class="iconify"></span>' +
'</li>' +
'<li>Block icon:' +
' <iconify-icon data-icon="ic:baseline-account"></iconify-icon>' +
'</li>' +
'<li>Icon with wrong tag: <p class="iconify" data-icon="mdi:alert"></p></li>' +
'</ul></div>';
const items = findPlaceholders(node);
function testIcon(
name: IconifyIconName | null,
expectedFinder: IconifyFinder
): void {
const item = items.shift();
expect(item.name).to.be.eql(name);
expect(item.finder).to.be.equal(expectedFinder);
}
// Test all icons
testIcon(
{
provider: '',
prefix: 'mdi',
name: 'home',
},
iconifyFinder
);
testIcon(
{
provider: '',
prefix: 'mdi',
name: 'account',
},
iconifyFinder
);
testIcon(
{
provider: '',
prefix: 'ic',
name: 'baseline-account',
},
iconifyIconFinder
);
// End of list
expect(items.shift()).to.be.equal(void 0);
});
});

View File

@ -1,219 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
const expect = chai.expect;
// Dummy svgAttributes variable
const svgAttributes: Record<string, unknown> = {};
/**
* Copy attributes from placeholder to SVG.
*
* This is similar to code used in render.ts
*
* @param placeholderElement
* @param svg
*/
function copyData(placeholderElement, svg) {
const svgStyle = svg.style;
// Copy attributes from placeholder
const placeholderAttributes = placeholderElement.attributes;
for (let i = 0; i < placeholderAttributes.length; i++) {
const item = placeholderAttributes.item(i);
if (item) {
const name = item.name;
if (
name !== 'class' &&
name !== 'style' &&
svgAttributes[name] === void 0
) {
try {
svg.setAttribute(name, item.value);
} catch (err) {}
}
}
}
// Copy styles from placeholder
const placeholderStyle = placeholderElement.style;
for (let i = 0; i < placeholderStyle.length; i++) {
const attr = placeholderStyle[i];
const value = placeholderStyle[attr];
if (value !== '') {
svgStyle[attr] = value;
}
}
}
describe('Testing copying node data', () => {
it('Inline attributes', () => {
const node = getNode('node-attributes');
node.innerHTML = '<p title="Testing" data-foo="bar">Test</p>';
const source = node.querySelector('p');
const target = document.createElement('span');
copyData(source, target);
// Test title
expect(source.getAttribute('title')).to.be.equal(
'Testing',
'Source title should be set'
);
expect(target.getAttribute('title')).to.be.equal(
'Testing',
'Target title should be set'
);
// Test data-*
expect(source.getAttribute('data-foo')).to.be.equal(
'bar',
'Source data-foo should be set'
);
expect(target.getAttribute('data-foo')).to.be.equal(
'bar',
'Target data-foo should be set'
);
});
it('Inline style', () => {
const node = getNode('node-attributes');
node.innerHTML =
'<p style="color: red; border: 1px solid green; vertical-align: -.1em">Test</p>';
const source = node.querySelector('p');
const target = document.createElement('span');
copyData(source, target);
// Test color
expect(source.style.color).to.be.equal(
'red',
'Source color should be red'
);
expect(target.style.color).to.be.equal(
'red',
'Target color should be red'
);
// Test border width
expect(source.style.borderWidth).to.be.equal(
'1px',
'Source border width should be 1px'
);
expect(target.style.borderWidth).to.be.equal(
'1px',
'Target border width should be 1px'
);
// Test background color
expect(source.style.backgroundColor).to.be.equal(
'',
'Source background color should not be set'
);
expect(target.style.backgroundColor).to.be.equal(
'',
'Target background color should not be set'
);
});
it('DOM style', () => {
const node = getNode('node-attributes');
node.innerHTML = '<p>Test</p>';
const source = node.querySelector('p');
source.style.color = 'green';
source.style.border = '2px solid blue';
const target = document.createElement('span');
copyData(source, target);
// Test color
expect(source.style.color).to.be.equal(
'green',
'Source color should be green'
);
expect(target.style.color).to.be.equal(
'green',
'Target color should be green'
);
// Test border width
expect(source.style.borderWidth).to.be.equal(
'2px',
'Source border width should be 2px'
);
expect(target.style.borderWidth).to.be.equal(
'2px',
'Target border width should be 2px'
);
// Test background color
expect(source.style.backgroundColor).to.be.equal(
'',
'Source background color should not be set'
);
expect(target.style.backgroundColor).to.be.equal(
'',
'Target background color should not be set'
);
});
it('Overwriting source style before copy', () => {
const node = getNode('node-attributes');
node.innerHTML = '<p style="color: blue">Test</p>';
const source = node.querySelector('p');
// Overwrite inline style
source.style.color = 'purple';
const target = document.createElement('span');
copyData(source, target);
// Test color
expect(source.style.color).to.be.equal(
'purple',
'Source color should be purple'
);
expect(target.style.color).to.be.equal(
'purple',
'Target color should be purple'
);
});
it('Overwriting target style during copy', () => {
const node = getNode('node-attributes');
node.innerHTML = '<p style="color: blue">Test</p>';
const source = node.querySelector('p');
const target = document.createElement('span');
// Set target style
target.style.color = 'purple';
target.style.verticalAlign = '-0.2em';
copyData(source, target);
// Test color
expect(source.style.color).to.be.equal(
'blue',
'Source color should be blue'
);
expect(target.style.color).to.be.equal(
'blue',
'Target color should be blue'
);
// Test vertical-align
expect(source.style.verticalAlign).to.be.equal(
'',
'Source vartical-align should not be set'
);
expect(target.style.verticalAlign).to.be.equal(
'-0.2em',
'Target vertical-align should be set'
);
});
});

View File

@ -1,55 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode, setRoot } from './node';
import { listRootNodes } from '@iconify/iconify/lib/modules/root';
import {
initObserver,
pauseObserver,
} from '@iconify/iconify/lib/modules/observer';
const expect = chai.expect;
describe('Testing observer creation', () => {
it('Creating observer and triggering event', (done) => {
const node = getNode('observer-creation');
setRoot(node);
// Get node
const list = listRootNodes();
expect(list.length).to.be.equal(1);
const item = list[0];
expect(item.node).to.be.equal(node);
expect(item.observer).to.be.equal(void 0);
// Do test
let counter = 0;
node.innerHTML = '<div></div><ul><li>test</li><li>test2</li></ul>';
initObserver((root) => {
expect(root.node).to.be.equal(node);
counter++;
// Should be called only once
expect(counter).to.be.equal(1);
// Check if observer is paused
expect(item.observer).to.not.be.equal(void 0);
expect(item.observer.paused).to.be.equal(0);
// Pause observer
pauseObserver();
expect(item.observer.paused).to.be.equal(1);
done();
});
// Add few nodes to trigger observer
expect(item.observer).to.not.be.equal(void 0);
expect(item.observer.paused).to.be.equal(0);
node.querySelector('div').innerHTML =
'<span class="test">Some text</span><i>!</i>';
});
});

View File

@ -1,112 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode, setRoot } from './node';
import { elementFinderProperty } from '@iconify/iconify/lib/modules/element';
import {
initObserver,
pauseObserver,
resumeObserver,
} from '@iconify/iconify/lib/modules/observer';
const expect = chai.expect;
describe('Testing observer with DOM manipulation', () => {
it('Series of events', (done) => {
const node = getNode('observer-manipulation');
setRoot(node);
let counter = 0;
let waitingCallback: string | boolean = true;
node.innerHTML =
'<div></div><ul><li>test</li><li>test2</li></ul><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg); vertical-align: -0.125em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi-home" data-inline="false" class="iconify"><path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"></path></svg>';
initObserver((root) => {
expect(root.node).to.be.equal(node);
expect(waitingCallback).to.be.equal(true);
counter++;
switch (counter) {
case 1:
// Added few nodes
// Remove few nodes. It should not trigger event listener
waitingCallback = 'removing nodes';
(() => {
const item = node.querySelector('ul > li:last-child');
const parent = item.parentNode;
parent.removeChild(item);
// Set timer for next step to make sure callback is not called
setTimeout(() => {
// Add node. This should trigger callback
const newItem = document.createElement('li');
parent.appendChild(newItem);
waitingCallback = true;
}, 50);
})();
break;
case 2:
// Added one list item
// Pause observer
waitingCallback = 'pause test';
(() => {
const item = node.querySelector('ul > li:last-child');
pauseObserver();
item.innerHTML = '<string>Strong</strong> text!';
// Set timer for next step to make sure callback is not called
setTimeout(() => {
// Resume observer and wait a bit. Resuming observer should not trigger update
waitingCallback = 'resume test';
resumeObserver();
setTimeout(() => {
// Change text of item: should remove <strong> and add new text node
waitingCallback = true;
item.innerHTML = 'Weak text!';
}, 50);
}, 50);
})();
break;
case 3:
waitingCallback = 'attributes on ul';
(() => {
const item = node.querySelector('ul');
item.setAttribute('data-foo', 'bar');
// Set timer for next step to make sure callback is not called
setTimeout(() => {
waitingCallback = true;
const item = node.querySelector('svg');
item[elementFinderProperty] = true; // Add fake finder property to trigger update
item.setAttribute('data-icon', 'mdi-account');
item.setAttribute('data-rotate', '2');
}, 50);
})();
break;
case 4:
(() => {
// Test removing attribute from SVG
const item = node.querySelector('svg');
item.removeAttribute('data-rotate');
})();
break;
case 5:
done();
break;
default:
done('Unexpected callback call!');
}
});
// Add few nodes to trigger observer
node.querySelector('div').innerHTML =
'<span class="test">Some text</span><i>!</i>';
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,124 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode, setRoot } from './node';
import { addFinder } from '@iconify/iconify/lib/modules/finder';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { getStorage, addIconSet } from '@iconify/core/lib/storage/storage';
import { listRootNodes } from '@iconify/iconify/lib/modules/root';
import { scanDOM, scanElement } from '@iconify/iconify/lib/modules/scanner';
const expect = chai.expect;
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
describe('Scanning DOM', () => {
// Add mentioned icons to storage
const storage = getStorage('', 'mdi');
addIconSet(storage, {
prefix: 'mdi',
icons: {
'account-box': {
body: '<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account-cash': {
body: '<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'account': {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
'home': {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
});
// Sanity check before running tests
expect(listRootNodes()).to.be.eql([]);
it('Scan DOM with preloaded icons', () => {
const node = getNode('scan-dom');
setRoot(node);
node.innerHTML =
'<div><p>Testing scanning DOM (should render SVG!)</p><ul>' +
'<li>Valid icons:' +
' <span class="iconify" data-icon="mdi:home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify test-icon iconify--mdi-account" data-icon="mdi:account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icon:' +
' <iconify-icon data-icon="mdi-account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <iconify-icon data-icon="mdi:account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
'</ul></div>';
// Scan node
scanDOM();
// Find elements
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(4);
// Check root nodes list
const nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
});
it('Scan DOM with unattached root', () => {
const fakeNode = getNode('scan-dom');
setRoot(fakeNode);
const node = document.createElement('div');
node.innerHTML = '<span class="iconify" data-icon="mdi:home"></span>';
// Check root nodes list
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeNode);
// Scan node
scanElement(node);
// Find elements
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
// Make sure tempoary node was not added as root
nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeNode);
});
it('Scan DOM with icon as root', () => {
const fakeNode = getNode('scan-dom');
setRoot(fakeNode);
const node = document.createElement('span');
node.setAttribute('data-icon', 'mdi:home');
// Check root nodes list
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeNode);
// Scan node
scanElement(node);
// Check node
expect(node.tagName).to.be.equal('SPAN');
expect(node.innerHTML).to.be.equal('');
// Make sure tempoary node was not added as root
nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeNode);
});
});

View File

@ -1,280 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode, setRoot } from './node';
import { addFinder } from '@iconify/iconify/lib/modules/finder';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { getStorage, addIconSet } from '@iconify/core/lib/storage/storage';
import { listRootNodes } from '@iconify/iconify/lib/modules/root';
import { scanDOM } from '@iconify/iconify/lib/modules/scanner';
import {
initObserver,
observe,
stopObserving,
} from '@iconify/iconify/lib/modules/observer';
const expect = chai.expect;
describe('Observe DOM', () => {
const storage = getStorage('', 'mdi');
before(() => {
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
// Add mentioned icons to storage
addIconSet(storage, {
prefix: 'mdi',
icons: {
'account-box': {
body: '<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account-cash': {
body: '<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'account': {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
'home': {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
});
});
it('Basic test', (done) => {
const node = getNode('observe-dom');
const ignoredNode = getNode('observe-dom');
// Set root and init observer
setRoot(node);
initObserver(scanDOM);
// Test listRootNodes
const nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
expect(nodes[0].temporary).to.be.equal(false);
// Set HTML
node.innerHTML =
'<p>Testing observing DOM (should render SVG!)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
ignoredNode.innerHTML =
'<p>This node should be ignored</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
// Test nodes
setTimeout(() => {
// Find elements
let elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
elements = ignoredNode.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
0,
'Looks like document.body is observed!'
);
// Test for "home" icon contents
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
done();
}, 100);
});
it('Change icon', (done) => {
const node = getNode('observe-dom');
// Set root and init observer
setRoot(node);
initObserver(scanDOM);
// Set HTML
node.innerHTML =
'<p>Testing observing DOM (should render SVG!)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
// Test nodes
setTimeout(() => {
// Find elements
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
// Test for "home" icon contents
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
// Change icon
elements[0].setAttribute('data-icon', 'mdi:account');
// Test nodes after timer
setTimeout(() => {
// Find elements
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
// Test for "home" icon contents
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.be.equal(-1);
expect(node.innerHTML.indexOf('M12 4a4')).to.not.be.equal(-1);
done();
}, 100);
}, 100);
});
it('Adding node to observe', (done) => {
const baseNode = getNode('observe-dom');
const node = getNode('observe-dom');
// Set root and init observer
setRoot(baseNode);
initObserver(scanDOM);
// Test listRootNodes
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
// Observe another node
observe(node);
nodes = listRootNodes();
expect(nodes.length).to.be.equal(2);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
expect(nodes[1].node).to.be.equal(node);
expect(nodes[1].temporary).to.be.equal(false);
// Set HTML
baseNode.innerHTML =
'<p>Testing observing 2 nodes (1) (should render SVG!)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
node.innerHTML =
'<p>Testing observing 2 nodes (2) (should render SVG!)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
// Test nodes
setTimeout(() => {
// Find elements
let elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
elements = baseNode.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
// Test for "home" icon contents
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
expect(baseNode.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(
-1
);
done();
}, 100);
});
it('Adding node to observe after setting content', (done) => {
const baseNode = getNode('observe-dom');
const node = getNode('observe-dom');
// Set root and init observer
setRoot(baseNode);
initObserver(scanDOM);
// Test listRootNodes
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
// Set HTML
baseNode.innerHTML =
'<p>Testing observing 2 nodes (1) (should render SVG!)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
node.innerHTML =
'<p>Testing observing 2 nodes (2) (should render SVG!)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
// Observe node: should run scan on next tick
observe(node);
// Test nodes
setTimeout(() => {
// Find elements
let elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
elements = baseNode.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
// Test for "home" icon contents
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
expect(baseNode.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(
-1
);
done();
}, 100);
});
it('Stop observing node', (done) => {
const baseNode = getNode('observe-dom');
const node = getNode('observe-dom');
// Set root and init observer
setRoot(baseNode);
initObserver(scanDOM);
// Test listRootNodes
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
// Observe another node
observe(node);
nodes = listRootNodes();
expect(nodes.length).to.be.equal(2);
expect(nodes[0].node).to.be.equal(baseNode);
expect(nodes[0].temporary).to.be.equal(false);
expect(nodes[1].node).to.be.equal(node);
expect(nodes[1].temporary).to.be.equal(false);
// Stop observing baseNode
stopObserving(baseNode);
nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
expect(nodes[0].temporary).to.be.equal(false);
// Set HTML
baseNode.innerHTML =
'<p>Testing observing 2 nodes (1) (should NOT render SVG!)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
node.innerHTML =
'<p>Testing observing 2 nodes (2) (should render SVG!)</p>' +
'<span class="iconify" data-icon="mdi:home"></span>';
// Test nodes
setTimeout(() => {
// Find elements
let elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
elements = baseNode.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(0);
// Test for "home" icon contents
expect(node.innerHTML.indexOf('20v-6h4v6h5v')).to.not.be.equal(-1);
expect(baseNode.innerHTML.indexOf('20v-6h4v6h5v')).to.be.equal(-1);
done();
}, 100);
});
});

View File

@ -1,465 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode, setRoot } from './node';
import { addFinder } from '@iconify/iconify/lib/modules/finder';
import { FakeData, setFakeData, prepareQuery, sendQuery } from './fake-api';
import { setAPIModule } from '@iconify/core/lib/api/modules';
import { addAPIProvider } from '@iconify/core/lib/api/config';
import { finder as iconifyFinder } from '@iconify/iconify/lib/finders/iconify';
import { finder as iconifyIconFinder } from '@iconify/iconify/lib/finders/iconify-icon';
import { listRootNodes } from '@iconify/iconify/lib/modules/root';
import { scanDOM, scanElement } from '@iconify/iconify/lib/modules/scanner';
const expect = chai.expect;
let prefixCounter = 0;
function nextPrefix(): string {
return 'scan-dom-api-' + prefixCounter++;
}
describe('Scanning DOM with API', () => {
before(() => {
// Add finders
addFinder(iconifyFinder);
addFinder(iconifyIconFinder);
// Set API
setAPIModule('', {
prepare: prepareQuery,
send: sendQuery,
});
});
it('Scan DOM with API', (done) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
addAPIProvider(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Set icons, load them with various delay
const data1: FakeData = {
icons: ['home', 'account-cash'],
delay: 100,
data: {
prefix: prefix1,
icons: {
'account-cash': {
body: '<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'home': {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(provider, prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
delay: 500,
data: {
prefix: prefix2,
icons: {
'account-box': {
body: '<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account': {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(provider, prefix2, data2);
const node = getNode('scan-dom');
node.innerHTML =
'<div><p>Testing scanning DOM with API (should render SVG!)</p><ul>' +
'<li>Inline icons:' +
' <span class="iconify iconify-inline" data-icon="@' +
provider +
':' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify-inline test-icon iconify--mdi-account" data-icon="@' +
provider +
':' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <iconify-icon data-icon="@' +
provider +
':' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <i class="iconify-icon" data-icon="@' +
provider +
':' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></i>' +
'</li>' +
'</ul></div>';
// Scan DOM
setRoot(node);
scanDOM();
// Test listRootNodes
const nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
expect(nodes[0].temporary).to.be.equal(false);
// First API response should have loaded
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
2,
'Expected to find 2 rendered SVG elements'
);
}, 200);
// Second API response should have loaded
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
4,
'Expected to find 4 rendered SVG elements'
);
done();
}, 700);
});
it('Changing icon name before it loaded', (done) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
addAPIProvider(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Set icons, load them with various delay
const data1: FakeData = {
icons: ['home', 'account-cash'],
delay: 100,
data: {
prefix: prefix1,
icons: {
'account-cash': {
body: '<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'home': {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(provider, prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
delay: 500,
data: {
prefix: prefix2,
icons: {
'account-box': {
body: '<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account': {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(provider, prefix2, data2);
const data1b: FakeData = {
icons: ['account', 'account-box'],
delay: 800, // +100ms for first query
data: {
prefix: prefix1,
icons: {
'account-box': {
body: '<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account': {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(provider, prefix1, data1b);
const node = getNode('scan-dom');
node.innerHTML =
'<div><p>Testing scanning DOM with API: renamed icon (should render SVG!)</p><ul>' +
'<li>Default finder:' +
' <span class="iconify-inline first-icon" data-icon="@' +
provider +
':' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify-inline second-icon iconify--mdi-account" data-icon="@' +
provider +
':' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>IconifyIcon finder:' +
' <iconify-icon class="third-icon" data-icon="@' +
provider +
':' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <iconify-icon class="fourth-icon" data-icon="@' +
provider +
':' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
'</ul></div>';
// Scan DOM
setRoot(node);
scanDOM();
// Test listRootNodes
const nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(node);
expect(nodes[0].temporary).to.be.equal(false);
// Make sure no icons were rendered yet
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
0,
'Expected to find 0 rendered SVG elements'
);
// Change icon name
const icon = node.querySelector('iconify-icon[title]');
expect(icon).to.not.be.equal(null);
expect(icon.getAttribute('class')).to.be.equal('third-icon');
icon.setAttribute(
'data-icon',
'@' + provider + ':' + prefix1 + ':account'
);
// First API response should have loaded, but only 1 icon should have been rendered
setTimeout(() => {
// Loaded for prefix1: account-cash, home
// Loaded for prefix2: -
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1, '200ms delay error');
}, 200);
// Second API response should have loaded
setTimeout(() => {
// Loaded for prefix1: account-cash, home
// Loaded for prefix2: account, account-box
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(3, '700ms delay error');
}, 700);
// Renamed icon from first API response
setTimeout(() => {
// Loaded for prefix1: account-cash, home, account-box, account
// Loaded for prefix2: account, account-box
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(4, '1000ms delay error');
done();
}, 1100);
});
it('Changing icon name before it loaded to invalid name', (done) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set fake API hosts to make test reliable
addAPIProvider(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Set icons, load them with various delay
const data1: FakeData = {
icons: ['home', 'account-cash'],
delay: 100,
data: {
prefix: prefix1,
icons: {
'account-cash': {
body: '<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'home': {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(provider, prefix1, data1);
const data2: FakeData = {
icons: ['account', 'account-box'],
delay: 500,
data: {
prefix: prefix2,
icons: {
'account-box': {
body: '<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account': {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(provider, prefix2, data2);
const node = getNode('scan-dom');
node.innerHTML =
'<div><p>Testing scanning DOM with API: invalid name (should render 3 SVGs!)</p><ul>' +
'<li>Inline icons (2 valid):' +
' <span class="iconify" data-icon="@' +
provider +
':' +
prefix1 +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify test-icon iconify--mdi-account" data-icon="@' +
provider +
':' +
prefix2 +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons (1 valid):' +
' <iconify-icon data-icon="@' +
provider +
':' +
prefix1 +
':account-cash" title="&lt;Cash&gt;!"></iconify-icon>' +
' <iconify-icon data-icon="@' +
provider +
':' +
prefix2 +
':account-box" data-inline="true" data-rotate="2" data-width="42"></iconify-icon>' +
'</li>' +
'</ul></div>';
// Scan DOM
setRoot(node);
scanDOM();
// Change icon name
const icon = node.querySelector('iconify-icon[title]');
expect(icon).to.not.be.equal(null);
icon.setAttribute('data-icon', '@' + provider + ':foo');
// First API response should have loaded, but only 1 icon
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(1);
}, 200);
// Second API response should have loaded
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(3);
done();
}, 700);
});
it('Unattached DOM node', (done) => {
const fakeRoot = getNode('scan-dom-unattached');
const provider = nextPrefix();
const prefix = nextPrefix();
// Set fake API hosts to make test reliable
addAPIProvider(provider, {
resources: ['https://api1.local', 'https://api2.local'],
});
// Load icons after 100ms
const data: FakeData = {
icons: ['home'],
delay: 100,
data: {
prefix,
icons: {
home: {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
};
setFakeData(provider, prefix, data);
const node = document.createElement('div');
node.innerHTML =
'Icon:' +
' <span class="iconify" data-icon="@' +
provider +
':' +
prefix +
':home"></span>';
// Set root node, test nodes list
setRoot(fakeRoot);
// Test listRootNodes
let nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeRoot);
expect(nodes[0].temporary).to.be.equal(false);
// Scan different node
scanElement(node);
// Test listRootNodes
nodes = listRootNodes();
expect(nodes.length).to.be.equal(2);
expect(nodes[0].node).to.be.equal(fakeRoot);
expect(nodes[1].node).to.be.equal(node);
expect(nodes[1].temporary).to.be.equal(true);
// API response should have loaded
setTimeout(() => {
const elements = node.querySelectorAll('svg.iconify');
expect(elements.length).to.be.equal(
1,
'Expected to find 1 rendered SVG element'
);
// Test nodes list: temporary node should have been removed
nodes = listRootNodes();
expect(nodes.length).to.be.equal(1);
expect(nodes[0].node).to.be.equal(fakeRoot);
// Done
done();
}, 200);
});
});

View File

@ -1,52 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import Iconify from '@iconify/iconify/lib/iconify';
const expect = chai.expect;
const selector =
'span.iconify, i.iconify, span.iconify-inline, i.iconify-inline';
// Do not observe document.body!
Iconify.stopObserving(document.documentElement);
// Create node to observe
const observedNode = getNode('iconify-api');
const ignoredNode = getNode('iconify-api');
Iconify.observe(observedNode);
observedNode.innerHTML =
'<div><p>Testing Iconify with API (should render SVG!)</p><ul>' +
'<li>Inline icons:' +
' <span class="iconify-inline" data-icon="mdi:home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify iconify-inline test-icon iconify--mdi-account" data-icon="mdi:account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
'</li>' +
'<li>Block icons:' +
' <i class="iconify" data-icon="mdi:account-cash" title="&lt;Cash&gt;!"></i>' +
' <span class="iconify" data-icon="mdi:account-box" data-inline="true" data-rotate="2" data-width="42"></span>' +
'</li>' +
'</ul></div>';
ignoredNode.innerHTML =
'<div>This node should not have icons! <span class="iconify-inline" data-icon="mdi:home" style="color: red; box-shadow: 0 0 2px black;"></span>';
describe('Testing Iconify object with API', () => {
it('Rendering icons with API', () => {
// Icons should have been replaced by now
let list = observedNode.querySelectorAll(selector);
expect(list.length).to.be.equal(0);
list = observedNode.querySelectorAll('svg.iconify');
expect(list.length).to.be.equal(4);
// Icons in ignored node should not have been replaced
list = ignoredNode.querySelectorAll(selector);
expect(list.length).to.be.equal(1);
list = ignoredNode.querySelectorAll('svg.iconify');
expect(list.length).to.be.equal(0);
});
});

View File

@ -1,141 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import Iconify from '@iconify/iconify/lib/iconify';
const expect = chai.expect;
const selector =
'span.iconify, i.iconify, span.iconify-inline, i.iconify-inline';
const node1 = getNode('iconify-basic');
const node2 = getNode('iconify-basic');
// Do not observe document.body!
Iconify.stopObserving(document.documentElement);
// Set root node
Iconify.observe(node1);
describe('Testing Iconify object', () => {
const prefix = 'invalid-' + Date.now();
// Add mentioned icons to storage
Iconify.addCollection({
prefix,
icons: {
'account-box': {
body: '<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account-cash': {
body: '<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'account': {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
'home': {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
});
// Add one icon separately
Iconify.addIcon(prefix + ':id-test', {
body: '<defs><path id="ssvg-id-1st-place-medala" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medald" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalf" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalh" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalj" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalm" d="M.93.01h120.55v58.36H.93z"/><path d="M52.849 78.373v-3.908c3.681-.359 6.25-.958 7.703-1.798c1.454-.84 2.54-2.828 3.257-5.962h4.021v40.385h-5.437V78.373h-9.544z" id="ssvg-id-1st-place-medalp"/><linearGradient x1="49.998%" y1="-13.249%" x2="49.998%" y2="90.002%" id="ssvg-id-1st-place-medalb"><stop stop-color="#1E88E5" offset="13.55%"/><stop stop-color="#1565C0" offset="93.8%"/></linearGradient><linearGradient x1="26.648%" y1="2.735%" x2="77.654%" y2="105.978%" id="ssvg-id-1st-place-medalk"><stop stop-color="#64B5F6" offset="13.55%"/><stop stop-color="#2196F3" offset="94.62%"/></linearGradient><radialGradient cx="22.368%" cy="12.5%" fx="22.368%" fy="12.5%" r="95.496%" id="ssvg-id-1st-place-medalo"><stop stop-color="#FFEB3B" offset="29.72%"/><stop stop-color="#FBC02D" offset="95.44%"/></radialGradient></defs><g fill="none" fill-rule="evenodd"><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medalc" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medala"/></mask><path fill="url(#ssvg-id-1st-place-medalb)" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medalc)" d="M45.44 42.18h31.43l30-48.43H75.44z"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medale" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medald"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medale)" fill="#424242" fill-rule="nonzero"><path d="M101.23-3L75.2 39H50.85L77.11-3h24.12zm5.64-3H75.44l-30 48h31.42l30.01-48z"/></g></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medalg" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalf"/></mask><path d="M79 30H43c-4.42 0-8 3.58-8 8v16.04c0 2.17 1.8 3.95 4.02 3.96h.01c2.23-.01 4.97-1.75 4.97-3.96V44c0-1.1.9-2 2-2h30c1.1 0 2 .9 2 2v9.93c0 1.98 2.35 3.68 4.22 4.04c.26.05.52.08.78.08c2.21 0 4-1.79 4-4V38c0-4.42-3.58-8-8-8z" fill="#FDD835" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medalg)"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medali" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalh"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medali)" fill="#424242" fill-rule="nonzero"><path d="M79 32c3.31 0 6 2.69 6 6v16.04A2.006 2.006 0 0 1 82.59 56c-1.18-.23-2.59-1.35-2.59-2.07V44c0-2.21-1.79-4-4-4H46c-2.21 0-4 1.79-4 4v10.04c0 .88-1.64 1.96-2.97 1.96c-1.12-.01-2.03-.89-2.03-1.96V38c0-3.31 2.69-6 6-6h36zm0-2H43c-4.42 0-8 3.58-8 8v16.04c0 2.17 1.8 3.95 4.02 3.96h.01c2.23-.01 4.97-1.75 4.97-3.96V44c0-1.1.9-2 2-2h30c1.1 0 2 .9 2 2v9.93c0 1.98 2.35 3.68 4.22 4.04c.26.05.52.08.78.08c2.21 0 4-1.79 4-4V38c0-4.42-3.58-8-8-8z"/></g></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medall" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalj"/></mask><path fill="url(#ssvg-id-1st-place-medalk)" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medall)" d="M76.87 42.18H45.44l-30-48.43h31.43z"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medaln" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalm"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medaln)" fill="#424242" fill-rule="nonzero"><path d="M45.1-3l26.35 42H47.1L20.86-3H45.1zm1.77-3H15.44l30 48h31.42L46.87-6z"/></g></g><circle fill="url(#ssvg-id-1st-place-medalo)" fill-rule="nonzero" cx="64" cy="86" r="38"/><path d="M64 51c19.3 0 35 15.7 35 35s-15.7 35-35 35s-35-15.7-35-35s15.7-35 35-35zm0-3c-20.99 0-38 17.01-38 38s17.01 38 38 38s38-17.01 38-38s-17.01-38-38-38z" opacity=".2" fill="#424242" fill-rule="nonzero"/><path d="M47.3 63.59h33.4v44.4H47.3z"/><use fill="#000" xlink:href="#ssvg-id-1st-place-medalp"/><use fill="#FFA000" xlink:href="#ssvg-id-1st-place-medalp"/></g>',
width: 128,
height: 128,
});
it('Check iconExists', () => {
expect(Iconify.iconExists(prefix + ':' + 'account')).to.be.equal(true);
expect(Iconify.iconExists(prefix + ':' + 'missing')).to.be.equal(false);
expect(Iconify.iconExists(prefix + '-123:' + 'missing')).to.be.equal(
false
);
});
it('Check listIcons', () => {
expect(Iconify.listIcons('', prefix)).to.be.eql([
prefix + ':account-box',
prefix + ':account-cash',
prefix + ':account',
prefix + ':home',
prefix + ':id-test',
]);
});
it('Get SVG node', () => {
const node = Iconify.renderSVG(prefix + ':account', {
inline: true,
});
expect(node).to.not.be.equal(null);
const html = node.outerHTML;
expect(html.indexOf('<svg')).to.be.equal(0);
// Get HTML
const html2 = Iconify.renderHTML(prefix + ':account', {
inline: true,
});
expect(html2).to.be.equal(html);
// Make sure inline attribute was applied
expect(html2.indexOf('vertical-align: -0.125em;') === -1).to.be.equal(
false
);
});
it('Rendering icons without API', (done) => {
node1.innerHTML =
'<div><p>Testing Iconify without API (should render SVG!) and with titles</p>' +
' <span class="iconify-inline" data-icon="' +
prefix +
':home" style="color: red; box-shadow: 0 0 2px black;" title="Home Icon"></span>' +
' <i class="iconify-inline test-icon iconify--mdi-account" data-icon="' +
prefix +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false" title="Account Icon"></i>' +
' <i class="iconify" data-icon="' +
prefix +
':account-cash" title="&lt;Cash&gt;!"></i>' +
' <span class="iconify" data-icon="' +
prefix +
':account-box" data-inline="true" data-rotate="2" data-width="42" title="account-box"></span>' +
' <span class="iconify" data-icon="' +
prefix +
':id-test"></span>' +
'</div>';
node2.innerHTML =
'<div><p>This node should not be replaced</p>' +
'<span class="iconify" data-icon="' +
prefix +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>';
// Icons should not have been replaced yet
let list = node1.querySelectorAll(selector);
expect(list.length).to.be.equal(5);
list = node2.querySelectorAll(selector);
expect(list.length).to.be.equal(1);
// Check in ticks
setTimeout(() => {
setTimeout(() => {
list = node1.querySelectorAll(selector);
expect(list.length).to.be.equal(0);
list = node2.querySelectorAll(selector);
expect(list.length).to.be.equal(1);
// Test SVG with ID
const idTest = node1.querySelector('#ssvg-id-1st-place-medala');
expect(idTest).to.be.equal(null, 'Expecting ID to be replaced');
done();
});
});
});
});

View File

@ -1,126 +0,0 @@
import mocha from 'mocha';
import chai from 'chai';
import { getNode } from './node';
import Iconify from '@iconify/iconify/lib/iconify.without-api';
const expect = chai.expect;
const selector =
'span.iconify, i.iconify, span.iconify-inline, i.iconify-inline';
const node1 = getNode('iconify-basic');
const node2 = getNode('iconify-basic');
// Do not observe document.body!
Iconify.stopObserving(document.documentElement);
// Set root node
Iconify.observe(node1);
describe('Testing Iconify object (without API)', () => {
const prefix = 'invalid-' + Date.now();
// Add mentioned icons to storage
Iconify.addCollection({
prefix,
icons: {
'account-box': {
body: '<path d="M6 17c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6m9-9a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3M3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z" fill="currentColor"/>',
},
'account-cash': {
body: '<path d="M11 8c0 2.21-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4m0 6.72V20H0v-2c0-2.21 3.13-4 7-4c1.5 0 2.87.27 4 .72M24 20H13V3h11v17m-8-8.5a2.5 2.5 0 0 1 5 0a2.5 2.5 0 0 1-5 0M22 7a2 2 0 0 1-2-2h-3c0 1.11-.89 2-2 2v9a2 2 0 0 1 2 2h3c0-1.1.9-2 2-2V7z" fill="currentColor"/>',
},
'account': {
body: '<path d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4z" fill="currentColor"/>',
},
'home': {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
});
// Add one icon separately
Iconify.addIcon(prefix + ':id-test', {
body: '<defs><path id="ssvg-id-1st-place-medala" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medald" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalf" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalh" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalj" d="M.93.01h120.55v58.36H.93z"/><path id="ssvg-id-1st-place-medalm" d="M.93.01h120.55v58.36H.93z"/><path d="M52.849 78.373v-3.908c3.681-.359 6.25-.958 7.703-1.798c1.454-.84 2.54-2.828 3.257-5.962h4.021v40.385h-5.437V78.373h-9.544z" id="ssvg-id-1st-place-medalp"/><linearGradient x1="49.998%" y1="-13.249%" x2="49.998%" y2="90.002%" id="ssvg-id-1st-place-medalb"><stop stop-color="#1E88E5" offset="13.55%"/><stop stop-color="#1565C0" offset="93.8%"/></linearGradient><linearGradient x1="26.648%" y1="2.735%" x2="77.654%" y2="105.978%" id="ssvg-id-1st-place-medalk"><stop stop-color="#64B5F6" offset="13.55%"/><stop stop-color="#2196F3" offset="94.62%"/></linearGradient><radialGradient cx="22.368%" cy="12.5%" fx="22.368%" fy="12.5%" r="95.496%" id="ssvg-id-1st-place-medalo"><stop stop-color="#FFEB3B" offset="29.72%"/><stop stop-color="#FBC02D" offset="95.44%"/></radialGradient></defs><g fill="none" fill-rule="evenodd"><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medalc" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medala"/></mask><path fill="url(#ssvg-id-1st-place-medalb)" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medalc)" d="M45.44 42.18h31.43l30-48.43H75.44z"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medale" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medald"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medale)" fill="#424242" fill-rule="nonzero"><path d="M101.23-3L75.2 39H50.85L77.11-3h24.12zm5.64-3H75.44l-30 48h31.42l30.01-48z"/></g></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medalg" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalf"/></mask><path d="M79 30H43c-4.42 0-8 3.58-8 8v16.04c0 2.17 1.8 3.95 4.02 3.96h.01c2.23-.01 4.97-1.75 4.97-3.96V44c0-1.1.9-2 2-2h30c1.1 0 2 .9 2 2v9.93c0 1.98 2.35 3.68 4.22 4.04c.26.05.52.08.78.08c2.21 0 4-1.79 4-4V38c0-4.42-3.58-8-8-8z" fill="#FDD835" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medalg)"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medali" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalh"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medali)" fill="#424242" fill-rule="nonzero"><path d="M79 32c3.31 0 6 2.69 6 6v16.04A2.006 2.006 0 0 1 82.59 56c-1.18-.23-2.59-1.35-2.59-2.07V44c0-2.21-1.79-4-4-4H46c-2.21 0-4 1.79-4 4v10.04c0 .88-1.64 1.96-2.97 1.96c-1.12-.01-2.03-.89-2.03-1.96V38c0-3.31 2.69-6 6-6h36zm0-2H43c-4.42 0-8 3.58-8 8v16.04c0 2.17 1.8 3.95 4.02 3.96h.01c2.23-.01 4.97-1.75 4.97-3.96V44c0-1.1.9-2 2-2h30c1.1 0 2 .9 2 2v9.93c0 1.98 2.35 3.68 4.22 4.04c.26.05.52.08.78.08c2.21 0 4-1.79 4-4V38c0-4.42-3.58-8-8-8z"/></g></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medall" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalj"/></mask><path fill="url(#ssvg-id-1st-place-medalk)" fill-rule="nonzero" mask="url(#ssvg-id-1st-place-medall)" d="M76.87 42.18H45.44l-30-48.43h31.43z"/></g><g transform="translate(3 4)"><mask id="ssvg-id-1st-place-medaln" fill="#fff"><use xlink:href="#ssvg-id-1st-place-medalm"/></mask><g opacity=".2" mask="url(#ssvg-id-1st-place-medaln)" fill="#424242" fill-rule="nonzero"><path d="M45.1-3l26.35 42H47.1L20.86-3H45.1zm1.77-3H15.44l30 48h31.42L46.87-6z"/></g></g><circle fill="url(#ssvg-id-1st-place-medalo)" fill-rule="nonzero" cx="64" cy="86" r="38"/><path d="M64 51c19.3 0 35 15.7 35 35s-15.7 35-35 35s-35-15.7-35-35s15.7-35 35-35zm0-3c-20.99 0-38 17.01-38 38s17.01 38 38 38s38-17.01 38-38s-17.01-38-38-38z" opacity=".2" fill="#424242" fill-rule="nonzero"/><path d="M47.3 63.59h33.4v44.4H47.3z"/><use fill="#000" xlink:href="#ssvg-id-1st-place-medalp"/><use fill="#FFA000" xlink:href="#ssvg-id-1st-place-medalp"/></g>',
width: 128,
height: 128,
});
it('Check iconExists', () => {
expect(Iconify.iconExists(prefix + ':' + 'account')).to.be.equal(true);
expect(Iconify.iconExists(prefix + ':' + 'missing')).to.be.equal(false);
expect(Iconify.iconExists(prefix + '-123:' + 'missing')).to.be.equal(
false
);
});
it('Get SVG node', () => {
const node = Iconify.renderSVG(prefix + ':account', {
inline: true,
});
expect(node).to.not.be.equal(null);
const html = node.outerHTML;
expect(html.indexOf('<svg')).to.be.equal(0);
// Get HTML
const html2 = Iconify.renderHTML(prefix + ':account', {
inline: true,
});
expect(html2).to.be.equal(html);
});
it('Rendering icons without API', (done) => {
node1.innerHTML =
'<div><p>Testing Iconify without API (should render SVG!)</p>' +
' <span class="iconify-inline" data-icon="' +
prefix +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>' +
' <i class="iconify-inline test-icon iconify--mdi-account" data-icon="' +
prefix +
':account" style="vertical-align: 0;" data-flip="horizontal" aria-hidden="false"></i>' +
' <i class="iconify" data-icon="' +
prefix +
':account-cash" title="&lt;Cash&gt;!"></i>' +
' <span class="iconify" data-icon="' +
prefix +
':account-box" data-inline="true" data-rotate="2" data-width="42"></span>' +
' <span class="iconify" data-icon="' +
prefix +
':id-test"></span>' +
'</div>';
node2.innerHTML =
'<div><p>This node should not be replaced</p>' +
'<span class="iconify" data-icon="' +
prefix +
':home" style="color: red; box-shadow: 0 0 2px black;"></span>';
// Icons should not have been replaced yet
let list = node1.querySelectorAll(selector);
expect(list.length).to.be.equal(5);
list = node2.querySelectorAll(selector);
expect(list.length).to.be.equal(1);
// Check in ticks
setTimeout(() => {
setTimeout(() => {
list = node1.querySelectorAll(selector);
expect(list.length).to.be.equal(0);
list = node2.querySelectorAll(selector);
expect(list.length).to.be.equal(1);
// Test SVG with ID
const idTest = node1.querySelector('#ssvg-id-1st-place-medala');
expect(idTest).to.be.equal(null, 'Expecting ID to be replaced');
done();
});
});
});
});

View File

@ -1,134 +0,0 @@
import { QueryModuleResponse } from '@iconify/api-redundancy';
import {
IconifyAPIIconsQueryParams,
IconifyAPIQueryParams,
IconifyAPIPrepareIconsQuery,
IconifyAPISendQuery,
} from '@iconify/core/lib/api/modules';
import { IconifyJSON } from '@iconify/types';
/**
* Fake data entry
*/
export interface FakeData {
icons: string[];
host?: string; // host to respond to
delay?: number; // 0 = instant reply
data: IconifyJSON; // data to send
}
/**
* Fake data storage
*/
const fakeData: Record<string, Record<string, FakeData[]>> = Object.create(
null
);
export function setFakeData(
provider: string,
prefix: string,
item: FakeData
): void {
if (fakeData[provider] === void 0) {
fakeData[provider] = Object.create(null);
}
const providerFakeData = fakeData[provider];
if (providerFakeData[prefix] === void 0) {
providerFakeData[prefix] = [];
}
providerFakeData[prefix].push(item);
}
interface FakeAPIQueryParams extends IconifyAPIIconsQueryParams {
data: FakeData;
}
/**
* Prepare params
*/
export const prepareQuery: IconifyAPIPrepareIconsQuery = (
provider: string,
prefix: string,
icons: string[]
): IconifyAPIIconsQueryParams[] => {
// Find items that have query
const items: IconifyAPIIconsQueryParams[] = [];
let missing = icons.slice(0);
if (fakeData[provider] === void 0) {
fakeData[provider] = Object.create(null);
}
const providerFakeData = fakeData[provider];
const type = 'icons';
if (providerFakeData[prefix] !== void 0) {
providerFakeData[prefix].forEach((item) => {
const matches = item.icons.filter(
(icon) => missing.indexOf(icon) !== -1
);
if (!matches.length) {
// No match
return;
}
// Contains at least one matching icon
missing = missing.filter((icon) => matches.indexOf(icon) === -1);
const query: FakeAPIQueryParams = {
type,
provider,
prefix,
icons: matches,
data: item,
};
items.push(query);
});
}
return items;
};
/**
* Load icons
*/
export const sendQuery: IconifyAPISendQuery = (
host: string,
params: IconifyAPIQueryParams,
callback: QueryModuleResponse
): void => {
if (params.type !== 'icons') {
// Fake API supports only icons
callback('abort', 400);
return;
}
const provider = params.provider;
const prefix = params.prefix;
const icons = params.icons;
const data = (params as FakeAPIQueryParams).data;
if (!data) {
throw new Error('Fake data is missing in query params');
}
if (typeof data.host === 'string' && data.host !== host) {
// Host mismatch - send error (first parameter = undefined)
callback('abort', 404);
return;
}
const sendResponse = () => {
console.log(
'Sending data for prefix "' +
(provider === '' ? '' : '@' + provider + ':') +
prefix +
'", icons:',
icons
);
callback('success', data.data);
};
if (!data.delay) {
sendResponse();
} else {
setTimeout(sendResponse, data.delay);
}
};

View File

@ -1,30 +0,0 @@
import { addRootNode, listRootNodes } from '@iconify/iconify/lib/modules/root';
import { stopObserving } from '@iconify/iconify/lib/modules/observer';
import { ObservedNode } from '@iconify/iconify/lib/modules/observed-node';
let counter = 0;
/**
* Create node for test
*/
export function getNode(prefix = 'test') {
const id = prefix + '-' + Date.now() + '-' + counter++;
const node = document.createElement('div');
node.setAttribute('id', id);
document.getElementById('debug').appendChild(node);
return node;
}
/**
* Set root node, remove old nodes
*/
export function setRoot(node: HTMLElement): ObservedNode {
listRootNodes().forEach((node) => {
if (typeof node.node !== 'function') {
stopObserving(node.node);
}
});
return addRootNode(node);
}

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
<meta name="viewport" content="width=1024" />
<title>Tests</title>
<link href="../node_modules/mocha/mocha.css" rel="stylesheet" />
<style>
#debug {
display: none;
}
#debug > div {
margin: 8px 0;
border: 1px solid #ccc;
padding: 8px;
}
</style>
</head>
<body>
<div id="mocha"></div>
<div id="debug"></div>
<script src="../node_modules/mocha/mocha.js"></script>
<script src="../node_modules/chai/chai.js"></script>
<script>
mocha.setup('bdd');
</script>
<!-- tests -->
<script class="mocha-exec">
mocha.run();
</script>
</body>
</html>

View File

@ -1,14 +0,0 @@
{
"compilerOptions": {
"rootDir": "./tests",
"outDir": "./lib",
"target": "ES2019",
"module": "ESNext",
"declaration": false,
"sourceMap": false,
"strict": false,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -75,11 +75,13 @@
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^4.0.0",
"@types/jest": "^27.4.1",
"@types/jsdom": "^16.2.14",
"@types/node": "^17.0.22",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"cross-env": "^7.0.3",
"eslint": "^8.11.0",
"jest": "^28.0.0-alpha.7",
"jsdom": "^19.0.0",
"rimraf": "^3.0.2",
"rollup": "^2.70.1",
"rollup-plugin-terser": "^7.0.2",

View File

@ -11,24 +11,29 @@ import {
} from '@iconify/core/lib/storage/functions';
import type { IconifyIconBuildResult } from '@iconify/utils/lib/svg/build';
import { iconToSVG } from '@iconify/utils/lib/svg/build';
import { renderIconInPlaceholder } from './modules/render';
import { initObserver } from './modules/observer';
import { scanDOM, scanElement } from './modules/scanner';
// Finders
import { addFinder } from './modules/finder';
import { finder as iconifyFinder } from './finders/iconify';
import { addBodyNode } from './modules/root';
// import { finder as iconifyIconFinder } from './finders/iconify-icon';
import { initObserver } from './observer/index';
import { scanDOM, scanElement } from './scanner/index';
import { addBodyNode } from './observer/root';
import { renderInlineSVG } from './render/svg';
/**
* Generate icon
*/
function generateIcon(
name: string,
customisations: IconifyIconCustomisations | undefined,
returnString: false | undefined
): SVGSVGElement | null;
function generateIcon(
name: string,
customisations: IconifyIconCustomisations | undefined,
returnString: true
): string | null;
function generateIcon(
name: string,
customisations?: IconifyIconCustomisations,
returnString?: boolean
): SVGElement | string | null {
returnString = false
): SVGSVGElement | string | null {
// Get icon data
const iconData = getIconData(name);
if (!iconData) {
@ -45,14 +50,18 @@ function generateIcon(
);
// Get data
return renderIconInPlaceholder(
const result = renderInlineSVG(
document.createElement('span'),
{
name: iconName,
name,
icon: iconName,
customisations: changes,
},
changes,
iconData,
returnString
) as unknown as SVGElement | string | null;
iconData
);
return returnString
? result.outerHTML
: (result as unknown as SVGSVGElement);
}
/**
@ -183,10 +192,6 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
// Add document.body node
addBodyNode();
// Add finder modules
// addFinder(iconifyIconFinder);
addFinder(iconifyFinder);
interface WindowWithIconifyStuff {
IconifyPreload?: IconifyJSON[] | IconifyJSON;
}

View File

@ -1,39 +0,0 @@
import type { IconifyFinder } from './interface';
import type { IconifyElement } from '../modules/element';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
import { finder as iconifyFinder } from './iconify';
const selector = 'iconify-icon';
const selectors = selector + ', i.' + selector + ', span.' + selector;
/**
* Export finder for:
* <iconify-icon />
*/
const finder: IconifyFinder = {
/**
* Find all elements
*/
find: (root: HTMLElement): NodeList => root.querySelectorAll(selectors),
/**
* Get icon name from element
*/
name: iconifyFinder.name,
/**
* Get customisations list from element
*/
customisations: (node: IconifyElement): IconifyIconCustomisations => {
return iconifyFinder.customisations(node, {
inline: false,
});
},
/**
* Filter classes
*/
classFilter: iconifyFinder.classFilter,
};
export { finder };

View File

@ -1,39 +0,0 @@
import type { IconifyFinder } from './interface';
import type { IconifyElement } from '../modules/element';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
import { finder as iconifyFinder } from './iconify-v1';
const selector = 'iconify-icon';
const selectors = selector + ', i.' + selector + ', span.' + selector;
/**
* Export finder for:
* <iconify-icon />
*/
const finder: IconifyFinder = {
/**
* Find all elements
*/
find: (root: HTMLElement): NodeList => root.querySelectorAll(selectors),
/**
* Get icon name from element
*/
name: iconifyFinder.name,
/**
* Get customisations list from element
*/
customisations: (node: IconifyElement): IconifyIconCustomisations => {
return iconifyFinder.customisations(node, {
inline: false,
});
},
/**
* Filter classes
*/
classFilter: iconifyFinder.classFilter,
};
export { finder };

View File

@ -1,161 +0,0 @@
import type { IconifyFinder } from './interface';
import type { IconifyElement } from '../modules/element';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
import { rotateFromString } from '@iconify/utils/lib/customisations/rotate';
import {
flipFromString,
alignmentFromString,
} from '@iconify/utils/lib/customisations/shorthand';
/**
* Check if attribute exists
*/
function hasAttribute(element: IconifyElement, key: string) {
return element.hasAttribute(key);
}
/**
* Get attribute value
*/
function getAttribute(element: IconifyElement, key: string): string {
return element.getAttribute(key) as string;
}
/**
* Get attribute value
*/
function getBooleanAttribute(
element: IconifyElement,
key: string
): boolean | null {
const value = element.getAttribute(key) as string;
if (value === key || value === 'true') {
return true;
}
if (value === '' || value === 'false') {
return false;
}
return null;
}
/**
* Boolean attributes
*/
const booleanAttributes: (keyof IconifyIconCustomisations)[] = [
'inline',
'hFlip',
'vFlip',
];
/**
* String attributes
*/
const stringAttributes: (keyof IconifyIconCustomisations)[] = [
'width',
'height',
];
/**
* Class names
*/
const mainClass = 'iconify';
/**
* Selector combining class names and tags
*/
const selector = 'i.' + mainClass + ', span.' + mainClass;
/**
* Export finder for:
* <span class="iconify" />
* <i class="iconify" />
* <span class="iconify-inline" />
* <i class="iconify-inline" />
*/
const finder: IconifyFinder = {
/**
* Find all elements
*/
find: (root: HTMLElement): NodeList => root.querySelectorAll(selector),
/**
* Get icon name from element
*/
name: (element: IconifyElement): string | null => {
if (hasAttribute(element, 'data-icon')) {
return getAttribute(element, 'data-icon');
}
return null;
},
/**
* Get customisations list from element
*/
customisations: (
element: IconifyElement,
defaultValues: IconifyIconCustomisations = {
inline: true,
}
): IconifyIconCustomisations => {
const result: IconifyIconCustomisations = defaultValues;
// Rotation
if (hasAttribute(element, 'data-rotate')) {
const value = rotateFromString(
getAttribute(element, 'data-rotate')
);
if (value) {
result.rotate = value;
}
}
// Shorthand attributes
if (hasAttribute(element, 'data-flip')) {
flipFromString(result, getAttribute(element, 'data-flip'));
}
if (hasAttribute(element, 'data-align')) {
alignmentFromString(result, getAttribute(element, 'data-align'));
}
// Boolean attributes
booleanAttributes.forEach((attr) => {
if (hasAttribute(element, 'data-' + attr)) {
const value = getBooleanAttribute(element, 'data-' + attr);
if (typeof value === 'boolean') {
(result as Record<string, boolean>)[attr] = value;
}
}
});
// String attributes
stringAttributes.forEach((attr) => {
if (hasAttribute(element, 'data-' + attr)) {
const value = getAttribute(element, 'data-' + attr);
if (value !== '') {
(result as Record<string, string>)[attr] = value;
}
}
});
return result;
},
/**
* Filter classes
*/
classFilter: (classList: string[]): string[] => {
const result: string[] = [];
classList.forEach((className) => {
if (
className !== 'iconify' &&
className !== '' &&
className.slice(0, 9) !== 'iconify--'
) {
result.push(className);
}
});
return result;
},
};
export { finder };

View File

@ -1,177 +0,0 @@
import type { IconifyFinder } from './interface';
import type { IconifyElement } from '../modules/element';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
import { rotateFromString } from '@iconify/utils/lib/customisations/rotate';
import {
flipFromString,
alignmentFromString,
} from '@iconify/utils/lib/customisations/shorthand';
/**
* Check if attribute exists
*/
function hasAttribute(element: IconifyElement, key: string) {
return element.hasAttribute(key);
}
/**
* Get attribute value
*/
function getAttribute(element: IconifyElement, key: string): string {
return element.getAttribute(key) as string;
}
/**
* Get attribute value
*/
function getBooleanAttribute(
element: IconifyElement,
key: string
): boolean | null {
const value = element.getAttribute(key) as string;
if (value === key || value === 'true') {
return true;
}
if (value === '' || value === 'false') {
return false;
}
return null;
}
/**
* Boolean attributes
*/
const booleanAttributes: (keyof IconifyIconCustomisations)[] = [
'inline',
'hFlip',
'vFlip',
];
/**
* String attributes
*/
const stringAttributes: (keyof IconifyIconCustomisations)[] = [
'width',
'height',
];
/**
* Class names
*/
const mainClass = 'iconify';
const inlineClass = 'iconify-inline';
/**
* Selector combining class names and tags
*/
const selector =
'i.' +
mainClass +
', span.' +
mainClass +
', i.' +
inlineClass +
', span.' +
inlineClass;
/**
* Export finder for:
* <span class="iconify" />
* <i class="iconify" />
* <span class="iconify-inline" />
* <i class="iconify-inline" />
*/
const finder: IconifyFinder = {
/**
* Find all elements
*/
find: (root: HTMLElement): NodeList => root.querySelectorAll(selector),
/**
* Get icon name from element
*/
name: (element: IconifyElement): string | null => {
if (hasAttribute(element, 'data-icon')) {
return getAttribute(element, 'data-icon');
}
return null;
},
/**
* Get customisations list from element
*/
customisations: (
element: IconifyElement,
defaultValues: IconifyIconCustomisations = {
inline: false,
}
): IconifyIconCustomisations => {
const result: IconifyIconCustomisations = defaultValues;
// Check class list for inline class
const className = element.getAttribute('class');
const classList = className ? className.split(/\s+/) : [];
if (classList.indexOf(inlineClass) !== -1) {
result.inline = true;
}
// Rotation
if (hasAttribute(element, 'data-rotate')) {
const value = rotateFromString(
getAttribute(element, 'data-rotate')
);
if (value) {
result.rotate = value;
}
}
// Shorthand attributes
if (hasAttribute(element, 'data-flip')) {
flipFromString(result, getAttribute(element, 'data-flip'));
}
if (hasAttribute(element, 'data-align')) {
alignmentFromString(result, getAttribute(element, 'data-align'));
}
// Boolean attributes
booleanAttributes.forEach((attr) => {
if (hasAttribute(element, 'data-' + attr)) {
const value = getBooleanAttribute(element, 'data-' + attr);
if (typeof value === 'boolean') {
(result as Record<string, boolean>)[attr] = value;
}
}
});
// String attributes
stringAttributes.forEach((attr) => {
if (hasAttribute(element, 'data-' + attr)) {
const value = getAttribute(element, 'data-' + attr);
if (value !== '') {
(result as Record<string, string>)[attr] = value;
}
}
});
return result;
},
/**
* Filter classes
*/
classFilter: (classList: string[]): string[] => {
const result: string[] = [];
classList.forEach((className) => {
if (
className !== 'iconify' &&
className !== '' &&
className.slice(0, 9) !== 'iconify--'
) {
result.push(className);
}
});
return result;
},
};
export { finder };

View File

@ -1,41 +0,0 @@
import type { IconifyElement } from '../modules/element';
import type { IconifyIconName } from '@iconify/utils/lib/icon/name';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
/**
* find - find elements that match plugin within root element
*/
export type IconifyFinderFind = (root: HTMLElement) => NodeList;
/**
* name - get icon name from element
*/
export type IconifyFinderName = (
element: IconifyElement
) => IconifyIconName | string | null;
/**
* customisations - get icon customisations
*/
export type IconifyFinderCustomisations = (
element: IconifyElement,
defaultVaues?: IconifyIconCustomisations
) => IconifyIconCustomisations;
/**
* classes - filter class list
*/
export type IconifyFinderClassFilter = (
// Classes to filter
classList: string[]
) => string[];
/**
* Interface for finder module
*/
export interface IconifyFinder {
find: IconifyFinderFind;
name: IconifyFinderName;
customisations: IconifyFinderCustomisations;
classFilter: IconifyFinderClassFilter;
}

View File

@ -0,0 +1,11 @@
/**
* Execute function when DOM is ready
*/
export function onReady(callback: () => void): void {
const doc = document;
if (doc.readyState && doc.readyState !== 'loading') {
callback();
} else {
doc.addEventListener('DOMContentLoaded', callback);
}
}

View File

@ -73,7 +73,7 @@ import {
stopObserving,
pauseObserver,
resumeObserver,
} from './modules/observer';
} from './observer/index';
/**
* Export required types

View File

@ -29,7 +29,7 @@ import {
stopObserving,
pauseObserver,
resumeObserver,
} from './modules/observer';
} from './observer/index';
/**
* Export required types

View File

@ -1,42 +0,0 @@
import type { IconifyIconName } from '@iconify/utils/lib/icon/name';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
import type { IconifyFinder } from '../finders/interface';
/**
* Icon status
*/
type IconStatus = 'missing' | 'loading' | 'loaded';
/**
* Data added to element to keep track of attribute changes
*/
export interface IconifyElementData {
name: IconifyIconName;
status: IconStatus;
customisations: IconifyIconCustomisations;
}
/**
* Extend Element type to allow TypeScript understand added properties
*/
interface IconifyElementStoredFinder {
iconifyFinder: IconifyFinder;
}
interface IconifyElementStoredData {
iconifyData: IconifyElementData;
}
export interface IconifyElement
extends HTMLElement,
IconifyElementStoredData,
IconifyElementStoredFinder {}
/**
* Names of properties to add to nodes
*/
export const elementFinderProperty: keyof IconifyElementStoredFinder =
('iconifyFinder' + Date.now()) as keyof IconifyElementStoredFinder;
export const elementDataProperty: keyof IconifyElementStoredData =
('iconifyData' + Date.now()) as keyof IconifyElementStoredData;

View File

@ -1,148 +0,0 @@
import {
elementFinderProperty,
IconifyElement,
elementDataProperty,
} from './element';
import {
IconifyIconName,
stringToIcon,
validateIcon,
} from '@iconify/utils/lib/icon/name';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
import type { IconifyFinder } from '../finders/interface';
/**
* List of modules
*/
const finders: IconifyFinder[] = [];
/**
* Add module
*/
export function addFinder(finder: IconifyFinder): void {
if (finders.indexOf(finder) === -1) {
finders.push(finder);
}
}
/**
* Interface for found elements list
*/
export interface PlaceholderElement {
name: IconifyIconName;
element?: IconifyElement;
finder?: IconifyFinder;
customisations?: IconifyIconCustomisations;
}
/**
* Clean icon name: convert from string if needed and validate
*/
export function cleanIconName(
name: IconifyIconName | string | null
): IconifyIconName | null {
if (typeof name === 'string') {
name = stringToIcon(name);
}
return name === null || !validateIcon(name) ? null : name;
}
/**
* Compare customisations. Returns true if identical
*/
function compareCustomisations(
list1: IconifyIconCustomisations,
list2: IconifyIconCustomisations
): boolean {
const keys1 = Object.keys(list1) as (keyof IconifyIconCustomisations)[];
const keys2 = Object.keys(list2) as (keyof IconifyIconCustomisations)[];
if (keys1.length !== keys2.length) {
return false;
}
for (let i = 0; i < keys1.length; i++) {
const key = keys1[i];
if (list2[key] !== list1[key]) {
return false;
}
}
return true;
}
/**
* Find all placeholders
*/
export function findPlaceholders(root: HTMLElement): PlaceholderElement[] {
const results: PlaceholderElement[] = [];
finders.forEach((finder) => {
const elements = finder.find(root);
Array.prototype.forEach.call(elements, (item) => {
const element = item as IconifyElement;
if (
element[elementFinderProperty] !== void 0 &&
element[elementFinderProperty] !== finder
) {
// Element is assigned to a different finder
return;
}
// Get icon name
const name = cleanIconName(finder.name(element));
if (name === null) {
// Invalid name - do not assign this finder to element
return;
}
// Assign finder to element and add it to results
element[elementFinderProperty] = finder;
const placeholder: PlaceholderElement = {
element,
finder,
name,
};
results.push(placeholder);
});
});
// Find all modified SVG
const elements = root.querySelectorAll('svg.iconify');
Array.prototype.forEach.call(elements, (item) => {
const element = item as IconifyElement;
const finder = element[elementFinderProperty];
const data = element[elementDataProperty];
if (!finder || !data) {
return;
}
// Get icon name
const name = cleanIconName(finder.name(element));
if (name === null) {
// Invalid name
return;
}
let updated = false;
let customisations;
if (name.prefix !== data.name.prefix || name.name !== data.name.name) {
updated = true;
} else {
customisations = finder.customisations(element);
if (!compareCustomisations(data.customisations, customisations)) {
updated = true;
}
}
// Add item to results
if (updated) {
const placeholder: PlaceholderElement = {
element,
finder,
name,
customisations,
};
results.push(placeholder);
}
});
return results;
}

View File

@ -1,21 +0,0 @@
// Fake interface to test old IE properties
interface OldIEElement extends HTMLElement {
doScroll?: boolean;
}
/**
* Execute function when DOM is ready
*/
export function onReady(callback: () => void): void {
const doc = document;
if (
doc.readyState === 'complete' ||
(doc.readyState !== 'loading' &&
!(doc.documentElement as OldIEElement).doScroll)
) {
callback();
} else {
doc.addEventListener('DOMContentLoaded', callback);
window.addEventListener('load', callback);
}
}

View File

@ -1,135 +0,0 @@
import type { FullIconifyIcon } from '@iconify/utils/lib/icon';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
import {
mergeCustomisations,
defaults,
} from '@iconify/utils/lib/customisations';
import { iconToSVG } from '@iconify/utils/lib/svg/build';
import { replaceIDs } from '@iconify/utils/lib/svg/id';
import type { PlaceholderElement } from './finder';
import type { IconifyElement, IconifyElementData } from './element';
import { elementDataProperty, elementFinderProperty } from './element';
/**
* Replace element with SVG
*/
export function renderIconInPlaceholder(
placeholder: PlaceholderElement,
customisations: IconifyIconCustomisations,
iconData: FullIconifyIcon,
returnString?: boolean
): IconifyElement | string | null {
// Create placeholder. Why placeholder? IE11 doesn't support innerHTML method on SVG.
let span: HTMLSpanElement;
try {
span = document.createElement('span');
} catch (err) {
return returnString ? '' : null;
}
const data = iconToSVG(
iconData,
mergeCustomisations(defaults, customisations)
);
// Placeholder properties
const placeholderElement = placeholder.element;
const finder = placeholder.finder;
const name = placeholder.name;
// Get class name
const placeholderClassName = placeholderElement
? placeholderElement.getAttribute('class')
: '';
const filteredClassList = finder
? finder.classFilter(
placeholderClassName ? placeholderClassName.split(/\s+/) : []
)
: [];
const className =
'iconify iconify--' +
name.prefix +
(name.provider === '' ? '' : ' iconify--' + name.provider) +
(filteredClassList.length ? ' ' + filteredClassList.join(' ') : '');
// Generate SVG as string
const html =
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="' +
className +
'">' +
replaceIDs(data.body) +
'</svg>';
// Set HTML for placeholder
span.innerHTML = html;
// Get SVG element
const svg = span.childNodes[0] as IconifyElement;
const svgStyle = svg.style;
// Add attributes
const svgAttributes = data.attributes as Record<string, string>;
Object.keys(svgAttributes).forEach((attr) => {
svg.setAttribute(attr, svgAttributes[attr]);
});
// Add custom styles
if (data.inline) {
svgStyle.verticalAlign = '-0.125em';
}
// Copy stuff from placeholder
if (placeholderElement) {
// Copy attributes
const placeholderAttributes = placeholderElement.attributes;
for (let i = 0; i < placeholderAttributes.length; i++) {
const item = placeholderAttributes.item(i);
if (item) {
const name = item.name;
if (
name !== 'class' &&
name !== 'style' &&
svgAttributes[name] === void 0
) {
try {
svg.setAttribute(name, item.value);
} catch (err) {
//
}
}
}
}
// Copy styles
const placeholderStyle = placeholderElement.style;
for (let i = 0; i < placeholderStyle.length; i++) {
const attr = placeholderStyle[i];
svgStyle[attr] = placeholderStyle[attr];
}
}
// Store finder specific data
if (finder) {
const elementData: IconifyElementData = {
name: name,
status: 'loaded',
customisations: customisations,
};
svg[elementDataProperty] = elementData;
svg[elementFinderProperty] = finder;
}
// Get result
const result = returnString ? span.innerHTML : svg;
// Replace placeholder
if (placeholderElement && placeholderElement.parentNode) {
placeholderElement.parentNode.replaceChild(svg, placeholderElement);
} else {
// Placeholder has no parent? Remove SVG parent as well
span.removeChild(svg);
}
// Return new node
return result;
}

View File

@ -1,222 +0,0 @@
import type { IconifyIconName } from '@iconify/utils/lib/icon/name';
import {
getStorage,
getIconFromStorage,
} from '@iconify/core/lib/storage/storage';
import { isPending, loadIcons } from '@iconify/core/lib/api/icons';
import type { FullIconifyIcon } from '@iconify/utils/lib/icon';
import { findPlaceholders } from './finder';
import type { IconifyElementData } from './element';
import { elementDataProperty } from './element';
import { renderIconInPlaceholder } from './render';
import type { ObservedNode } from './observed-node';
import {
pauseObservingNode,
resumeObservingNode,
stopObserving,
observe,
} from './observer';
import { findRootNode, listRootNodes } from './root';
/**
* Flag to avoid scanning DOM too often
*/
let scanQueued = false;
/**
* Icons have been loaded
*/
function checkPendingIcons(): void {
if (!scanQueued) {
scanQueued = true;
setTimeout(() => {
if (scanQueued) {
scanQueued = false;
scanDOM();
}
});
}
}
/**
* Compare Icon objects. Returns true if icons are identical.
*
* Note: null means icon is invalid, so null to null comparison = false.
*/
const compareIcons = (
icon1: IconifyIconName | null,
icon2: IconifyIconName | null
): boolean => {
return (
icon1 !== null &&
icon2 !== null &&
icon1.name === icon2.name &&
icon1.prefix === icon2.prefix
);
};
/**
* Scan node for placeholders
*/
export function scanElement(root: HTMLElement): void {
// Add temporary node
const node = findRootNode(root);
if (!node) {
scanDOM(
{
node: root,
temporary: true,
},
true
);
} else {
scanDOM(node);
}
}
/**
* Scan DOM for placeholders
*/
export function scanDOM(node?: ObservedNode, addTempNode = false): void {
scanQueued = false;
// List of icons to load: [provider][prefix][name] = boolean
const iconsToLoad: Record<
string,
Record<string, Record<string, boolean>>
> = Object.create(null);
// Get placeholders
(node ? [node] : listRootNodes()).forEach((node) => {
const root = typeof node.node === 'function' ? node.node() : node.node;
if (!root || !root.querySelectorAll) {
return;
}
// Track placeholders
let hasPlaceholders = false;
// Observer
let paused = false;
// Find placeholders
findPlaceholders(root).forEach((item) => {
const element = item.element;
const iconName = item.name;
const provider = iconName.provider;
const prefix = iconName.prefix;
const name = iconName.name;
let data: IconifyElementData = element[elementDataProperty];
// Icon has not been updated since last scan
if (data !== void 0 && compareIcons(data.name, iconName)) {
// Icon name was not changed and data is set - quickly return if icon is missing or still loading
switch (data.status) {
case 'missing':
return;
case 'loading':
if (
isPending({
provider,
prefix,
name,
})
) {
// Pending
hasPlaceholders = true;
return;
}
}
}
// Check icon
const storage = getStorage(provider, prefix);
if (storage.icons[name] !== void 0) {
// Icon exists - pause observer before replacing placeholder
if (!paused && node.observer) {
pauseObservingNode(node);
paused = true;
}
// Get customisations
const customisations =
item.customisations !== void 0
? item.customisations
: item.finder.customisations(element);
// Render icon
renderIconInPlaceholder(
item,
customisations,
getIconFromStorage(storage, name) as FullIconifyIcon
);
return;
}
if (storage.missing[name]) {
// Mark as missing
data = {
name: iconName,
status: 'missing',
customisations: {},
};
element[elementDataProperty] = data;
return;
}
if (!isPending({ provider, prefix, name })) {
// Add icon to loading queue
if (iconsToLoad[provider] === void 0) {
iconsToLoad[provider] = Object.create(null);
}
const providerIconsToLoad = iconsToLoad[provider];
if (providerIconsToLoad[prefix] === void 0) {
providerIconsToLoad[prefix] = Object.create(null);
}
providerIconsToLoad[prefix][name] = true;
}
// Mark as loading
data = {
name: iconName,
status: 'loading',
customisations: {},
};
element[elementDataProperty] = data;
hasPlaceholders = true;
});
// Node stuff
if (node.temporary && !hasPlaceholders) {
// Remove temporary node
stopObserving(root);
} else if (addTempNode && hasPlaceholders) {
// Add new temporary node
observe(root, true);
} else if (paused && node.observer) {
// Resume observer
resumeObservingNode(node);
}
});
// Load icons
Object.keys(iconsToLoad).forEach((provider) => {
const providerIconsToLoad = iconsToLoad[provider];
Object.keys(providerIconsToLoad).forEach((prefix) => {
loadIcons(
Object.keys(providerIconsToLoad[prefix]).map((name) => {
const icon: IconifyIconName = {
provider,
prefix,
name,
};
return icon;
}),
checkPendingIcons
);
});
});
}

View File

@ -1,13 +1,12 @@
import type { IconifyElement } from './element';
import { elementFinderProperty } from './element';
import type { ObservedNode } from './observed-node';
import { elementDataProperty, IconifyElement } from '../scanner/config';
import type { ObservedNode } from './types';
import {
listRootNodes,
addRootNode,
findRootNode,
removeRootNode,
} from './root';
import { onReady } from './ready';
import { onReady } from '../helpers/ready';
/**
* Observer callback function
@ -64,7 +63,7 @@ function checkMutations(node: ObservedNode, mutations: MutationRecord[]): void {
(item.addedNodes && item.addedNodes.length > 0) ||
// Check for icon or placeholder with modified attributes
(item.type === 'attributes' &&
(item.target as IconifyElement)[elementFinderProperty] !==
(item.target as IconifyElement)[elementDataProperty] !==
void 0)
) {
if (!observer.paused) {
@ -94,8 +93,8 @@ function startObserver(node: ObservedNode): void {
}
const root = typeof node.node === 'function' ? node.node() : node.node;
if (!root) {
// document.body is not available yet
if (!root || !window) {
// document.body is not available yet or window is missing
return;
}
@ -107,7 +106,9 @@ function startObserver(node: ObservedNode): void {
}
// Create new instance, observe
observer.instance = new MutationObserver(checkMutations.bind(null, node));
observer.instance = new window.MutationObserver(
checkMutations.bind(null, node)
);
continueObserving(node, root);
// Scan immediately
@ -126,7 +127,7 @@ function startObservers(): void {
/**
* Stop observer
*/
function stopObserver(node: ObservedNode): void {
export function stopObserver(node: ObservedNode): void {
if (!node.observer) {
return;
}

View File

@ -1,4 +1,4 @@
import type { ObservedNode } from './observed-node';
import type { ObservedNode } from './types';
/**
* List of root nodes
@ -34,7 +34,7 @@ export function addRootNode(
return node;
}
// Create item, add it to list, start observer
// Create item, add it to list
node = {
node: root,
temporary: autoRemove,
@ -61,12 +61,12 @@ export function addBodyNode(): ObservedNode {
/**
* Remove root node
*/
export function removeRootNode(root: HTMLElement): void {
nodes = nodes.filter((node) => {
const element =
typeof node.node === 'function' ? node.node() : node.node;
return root !== element;
});
export function removeRootNode(root: HTMLElement | ObservedNode): void {
nodes = nodes.filter(
(node) =>
root !== node &&
root !== (typeof node.node === 'function' ? node.node() : node.node)
);
}
/**

View File

@ -0,0 +1,44 @@
import type { IconifyElement } from '../scanner/config';
/**
* Add classes to SVG, removing previously added classes, keeping custom classes
*/
export function applyClasses(
svg: IconifyElement,
classes: Set<string>,
previouslyAddedClasses: Set<string>,
placeholder?: IconifyElement
): string[] {
const svgClasses = svg.classList;
// Copy classes from placeholder
if (placeholder) {
const placeholderClasses = placeholder.classList;
Array.from(placeholderClasses).forEach((item) => {
svgClasses.add(item);
});
}
// Add new classes
const addedClasses: string[] = [];
classes.forEach((item: string) => {
if (!svgClasses.contains(item)) {
// Add new class
svgClasses.add(item);
addedClasses.push(item);
} else if (previouslyAddedClasses.has(item)) {
// Was added before: keep it
addedClasses.push(item);
}
});
// Remove previously added classes
previouslyAddedClasses.forEach((item) => {
if (!classes.has(item)) {
// Class that was added before, but no longer needed
svgClasses.remove(item);
}
});
return addedClasses;
}

View File

@ -0,0 +1,31 @@
import type {
IconifyElement,
IconifyElementChangedStyles,
} from '../scanner/config';
/**
* Copy old styles, apply new styles
*/
export function applyStyle(
svg: IconifyElement,
styles: Record<string, string>,
previouslyAddedStyles?: IconifyElementChangedStyles
): IconifyElementChangedStyles {
const svgStyle = svg.style;
// Remove previously added styles
(previouslyAddedStyles || []).forEach((prop) => {
svgStyle.removeProperty(prop);
});
// Apply new styles, ignoring styles that already exist
const appliedStyles: IconifyElementChangedStyles = [];
for (const prop in styles) {
if (!svgStyle.getPropertyValue(prop)) {
appliedStyles.push(prop);
svgStyle.setProperty(prop, styles[prop]);
}
}
return appliedStyles;
}

View File

@ -0,0 +1,102 @@
import type { FullIconifyIcon } from '@iconify/utils/lib/icon';
import { iconToSVG } from '@iconify/utils/lib/svg/build';
import { replaceIDs } from '@iconify/utils/lib/svg/id';
import {
elementDataProperty,
IconifyElement,
IconifyElementProps,
IconifyElementData,
} from '../scanner/config';
import { applyClasses } from './classes';
import { applyStyle } from './style';
/**
* Render icon as inline SVG
*/
export function renderInlineSVG(
element: IconifyElement,
props: IconifyElementProps,
iconData: FullIconifyIcon
): IconifyElement {
// Create placeholder. Why placeholder? innerHTML setter on SVG does not work in some environments.
let span: HTMLSpanElement;
try {
span = document.createElement('span');
} catch (err) {
return element;
}
// Generate data to render
const renderData = iconToSVG(iconData, props.customisations);
// Get old data
const oldData = element[elementDataProperty];
// Generate SVG
const html =
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img">' +
replaceIDs(renderData.body) +
'</svg>';
span.innerHTML = html;
// Get SVG element
const svg = span.childNodes[0] as IconifyElement;
// Add attributes
const svgAttributes = renderData.attributes as Record<string, string>;
Object.keys(svgAttributes).forEach((attr) => {
svg.setAttribute(attr, svgAttributes[attr]);
});
const placeholderAttributes = element.attributes;
for (let i = 0; i < placeholderAttributes.length; i++) {
const item = placeholderAttributes.item(i);
const name = item.name;
if (name !== 'class' && !svg.hasAttribute(name)) {
svg.setAttribute(name, item.value);
}
}
// Add classes
const classesToAdd: Set<string> = new Set(['iconify']);
const iconName = props.icon;
['provider', 'prefix'].forEach((attr: keyof typeof iconName) => {
if (iconName[attr]) {
classesToAdd.add('iconify--' + iconName[attr]);
}
});
const addedClasses = applyClasses(
svg,
classesToAdd,
new Set(oldData && oldData.addedClasses),
element
);
// Update style
const addedStyles = applyStyle(
svg,
renderData.inline
? {
'vertical-align': '-0.125em',
}
: {},
oldData && oldData.addedStyles
);
// Add data to element
const newData: IconifyElementData = {
...props,
isSVG: true,
status: 'loaded',
addedClasses,
addedStyles,
};
svg[elementDataProperty] = newData;
// Replace old element
if (element.parentNode) {
element.parentNode.replaceChild(svg, element);
}
return svg;
}

View File

@ -0,0 +1,24 @@
import type { IconifyElementProps } from './config';
import { defaults } from '@iconify/utils/lib/customisations';
/**
* Compare props
*/
export function propsChanged(
props1: IconifyElementProps,
props2: IconifyElementProps
): boolean {
if (props1.name !== props2.name) {
return true;
}
const customisations1 = props1.customisations;
const customisations2 = props2.customisations;
for (const key in defaults) {
if (customisations1[key] !== customisations2[key]) {
return true;
}
}
return false;
}

View File

@ -0,0 +1,73 @@
import type { IconifyIconName } from '@iconify/utils/lib/icon/name';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
/**
* Class names
*/
export const blockClass = 'iconify';
export const inlineClass = 'iconify-inline';
/**
* Data used to verify if icon is the same
*/
export interface IconifyElementProps {
// Icon name as string
name: string;
// Icon name as object
icon: IconifyIconName;
// Customisations
customisations: Required<IconifyIconCustomisations>;
}
/**
* Icon status
*/
type IconStatus = 'missing' | 'loading' | 'loaded';
/**
* List of classes added to element
*
* If class already exists in element, it is not included in list
*/
export type IconifyElementAddedClasses = string[];
/**
* List of added inline styles
*
* Style is not changed if custom value is set
*/
export type IconifyElementChangedStyles = string[];
/**
* Data added to element to keep track of changes
*/
export interface IconifyElementData extends IconifyElementProps {
// Status
status: IconStatus;
// True if SVG has been rendered
isSVG?: boolean;
// List of classes that were added to element on last render
addedClasses?: IconifyElementAddedClasses;
// List of changes to style on last render
addedStyles?: IconifyElementChangedStyles;
}
/**
* Extend Element type to allow TypeScript understand added properties
*/
interface IconifyElementStoredData {
iconifyData?: IconifyElementData;
}
export interface IconifyElement extends HTMLElement, IconifyElementStoredData {}
/**
* Names of properties to add to nodes
*/
export const elementDataProperty: keyof IconifyElementStoredData =
('iconifyData' + Date.now()) as keyof IconifyElementStoredData;

View File

@ -0,0 +1,57 @@
import {
blockClass,
elementDataProperty,
IconifyElement,
IconifyElementProps,
inlineClass,
} from './config';
import { getElementProps } from './get-props';
/**
* Selector combining class names and tags
*/
const selector =
'svg.' +
blockClass +
', i.' +
blockClass +
', span.' +
blockClass +
', i.' +
inlineClass +
', span.' +
inlineClass;
/**
* Found nodes
*/
export type ScannedNodesListItem = {
node: IconifyElement;
props: IconifyElementProps;
};
export type ScannedNodesList = ScannedNodesListItem[];
/**
* Find all parent nodes in DOM
*/
export function scanRootNode(root: HTMLElement): ScannedNodesList {
const nodes: ScannedNodesList = [];
root.querySelectorAll(selector).forEach((node: IconifyElement) => {
// Get props, ignore SVG rendered outside of SVG framework
const props =
node[elementDataProperty] || node.tagName.toLowerCase() !== 'svg'
? getElementProps(node)
: null;
if (props) {
nodes.push({
node,
props,
});
}
});
return nodes;
}

View File

@ -0,0 +1,107 @@
import { stringToIcon } from '@iconify/utils/lib/icon/name';
import { defaults } from '@iconify/utils/lib/customisations';
import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations';
import { rotateFromString } from '@iconify/utils/lib/customisations/rotate';
import {
alignmentFromString,
flipFromString,
} from '@iconify/utils/lib/customisations/shorthand';
import { inlineClass } from './config';
import type { IconifyElementProps } from './config';
/**
* Size attributes
*/
const sizeAttributes: (keyof IconifyIconCustomisations)[] = ['width', 'height'];
/**
* Boolean attributes
*/
const booleanAttributes: (keyof IconifyIconCustomisations)[] = [
'inline',
'hFlip',
'vFlip',
];
/**
* Combined attributes
*/
type CombinedAtttributeFunction = (
customisations: IconifyIconCustomisations,
value: string
) => void;
const combinedAttributes: Record<string, CombinedAtttributeFunction> = {
flip: flipFromString,
align: alignmentFromString,
};
/**
* Get attribute value
*/
function getBooleanAttribute(value: unknown, key: string): boolean | null {
if (value === key || value === 'true') {
return true;
}
if (value === '' || value === 'false') {
return false;
}
return null;
}
/**
* Get element properties from HTML element
*/
export function getElementProps(element: Element): IconifyElementProps | null {
// Get icon name
const name = element.getAttribute('data-icon');
const icon = typeof name === 'string' && stringToIcon(name, true);
if (!icon) {
return null;
}
// Get defaults
const customisations = {
...defaults,
};
// Get inline status
customisations.inline =
element.classList && element.classList.contains(inlineClass);
// Get dimensions
sizeAttributes.forEach((attr) => {
const value = element.getAttribute('data-' + attr);
if (value) {
customisations[attr as 'width'] = value;
}
});
// Get rotation
const rotation = element.getAttribute('data-rotate');
if (typeof rotation === 'string') {
customisations.rotate = rotateFromString(rotation);
}
// Get alignment and transformations shorthand attributes
for (const attr in combinedAttributes) {
const value = element.getAttribute('data-' + attr);
if (typeof value === 'string') {
combinedAttributes[attr](customisations, value);
}
}
// Boolean attributes
booleanAttributes.forEach((attr) => {
const key = 'data-' + attr;
const value = getBooleanAttribute(element.getAttribute(key), key);
if (typeof value === 'boolean') {
customisations[attr as 'inline'] = value;
}
});
return {
name,
icon,
customisations,
};
}

View File

@ -0,0 +1,230 @@
import {
getIconFromStorage,
getStorage,
} from '@iconify/core/lib/storage/storage';
import { isPending, loadIcons } from '@iconify/core/lib/api/icons';
import { findRootNode, listRootNodes } from '../observer/root';
import type { ObservedNode } from '../observer/types';
import { propsChanged } from './compare';
import {
elementDataProperty,
IconifyElement,
IconifyElementData,
IconifyElementProps,
} from './config';
import { scanRootNode } from './find';
import type { IconifyIconName } from '../iconify';
import type { FullIconifyIcon } from '@iconify/utils/lib/icon';
import { renderInlineSVG } from '../render/svg';
import {
observe,
pauseObservingNode,
resumeObservingNode,
stopObserving,
} from '../observer';
/**
* Flag to avoid scanning DOM too often
*/
let scanQueued = false;
/**
* Icons have been loaded
*/
function checkPendingIcons(): void {
if (!scanQueued) {
scanQueued = true;
setTimeout(() => {
if (scanQueued) {
scanQueued = false;
scanDOM();
}
});
}
}
/**
* Scan node for placeholders
*/
export function scanDOM(rootNode?: ObservedNode, addTempNode = false): void {
// List of icons to load: [provider][prefix] = Set<name>
const iconsToLoad: Record<
string,
Record<string, Set<string>>
> = Object.create(null);
/**
* Get status based on icon
*/
interface GetIconResult {
status: IconifyElementData['status'];
icon?: FullIconifyIcon;
}
function getIcon(icon: IconifyIconName, load: boolean): GetIconResult {
const { provider, prefix, name } = icon;
const storage = getStorage(provider, prefix);
if (storage.icons[name]) {
return {
status: 'loaded',
icon: getIconFromStorage(storage, name),
};
}
if (storage.missing[name]) {
return {
status: 'missing',
};
}
if (load && !isPending(icon)) {
const providerIconsToLoad =
iconsToLoad[provider] ||
(iconsToLoad[provider] = Object.create(null));
const set =
providerIconsToLoad[prefix] ||
(providerIconsToLoad[prefix] = new Set());
set.add(name);
}
return {
status: 'loading',
};
}
// Parse all root nodes
(rootNode ? [rootNode] : listRootNodes()).forEach((observedNode) => {
const root =
typeof observedNode.node === 'function'
? observedNode.node()
: observedNode.node;
if (!root || !root.querySelectorAll) {
return;
}
// Track placeholders
let hasPlaceholders = false;
// Observer
let paused = false;
/**
* Render icon
*/
function render(
element: IconifyElement,
props: IconifyElementProps,
iconData: FullIconifyIcon
) {
if (!paused) {
paused = true;
pauseObservingNode(observedNode);
}
renderInlineSVG(element, props, iconData);
}
// Find all elements
scanRootNode(root).forEach(({ node, props }) => {
// Check if item already has props
const oldData = node[elementDataProperty];
if (!oldData) {
// New icon without data
const { status, icon } = getIcon(props.icon, true);
if (icon) {
// Ready to render!
render(node, props, icon);
return;
}
// Loading or missing
hasPlaceholders = hasPlaceholders || status === 'loading';
node[elementDataProperty] = {
...props,
status,
};
return;
}
// Previously found icon
let item: GetIconResult;
if (!propsChanged(oldData, props)) {
// Props have not changed. Check status
const oldStatus = oldData.status;
if (oldStatus !== 'loading') {
return;
}
item = getIcon(props.icon, false);
if (!item.icon) {
// Nothing to render
oldData.status = item.status;
return;
}
} else {
// Properties have changed: load icon if name has changed
item = getIcon(props.icon, oldData.name !== props.name);
if (!item.icon) {
// Cannot render icon: update status and props
hasPlaceholders =
hasPlaceholders || item.status === 'loading';
Object.assign(oldData, {
...props,
status: item.status,
});
return;
}
}
// Re-render icon
render(node, props, item.icon);
});
// Observed node stuff
if (observedNode.temporary && !hasPlaceholders) {
// Remove temporary node
stopObserving(root);
} else if (addTempNode && hasPlaceholders) {
// Add new temporary node
observe(root, true);
} else if (paused && observedNode.observer) {
// Resume observer
resumeObservingNode(observedNode);
}
});
// Load icons
for (const provider in iconsToLoad) {
const providerIconsToLoad = iconsToLoad[provider];
for (const prefix in providerIconsToLoad) {
const set = providerIconsToLoad[prefix];
loadIcons(
Array.from(set).map((name) => ({
provider,
prefix,
name,
})),
checkPendingIcons
);
}
}
}
/**
* Scan node for placeholders
*/
export function scanElement(root: HTMLElement): void {
// Add temporary node
const node = findRootNode(root);
if (!node) {
scanDOM(
{
node: root,
temporary: true,
},
true
);
} else {
scanDOM(node);
}
}

View File

@ -1,18 +0,0 @@
import Iconify from '../';
describe('Testing Iconify API functions with Node.js', () => {
it('Cache functions', () => {
// All functions should fail, not without throwing exceptions
expect(Iconify.disableCache('all')).toBeUndefined();
expect(Iconify.enableCache('all')).toBeUndefined();
});
it('Adding API provider', () => {
// Add dummy provider. Should not throw exceptions and return true on success
expect(
Iconify.addAPIProvider('test', {
resources: ['http://localhost'],
})
).toBe(true);
});
});

View File

@ -0,0 +1,500 @@
import { cleanupGlobals, setupDOM } from './helpers';
import { getElementProps } from '../src/scanner/get-props';
import { propsChanged } from '../src/scanner/compare';
import { defaults } from '@iconify/utils/lib/customisations';
describe('Testing element properties', () => {
beforeEach(() => {
setupDOM('');
});
afterEach(cleanupGlobals);
it('Icon name', () => {
const element = document.createElement('span');
expect(getElementProps(element)).toBeNull();
// Set name
element.setAttribute('data-icon', 'mdi:home');
const props1 = getElementProps(element);
expect(props1).toEqual({
name: 'mdi:home',
icon: {
provider: '',
prefix: 'mdi',
name: 'home',
},
customisations: {
...defaults,
},
});
// More complex name
element.setAttribute('data-icon', '@custom-api:icon-prefix:icon-name');
const props2 = getElementProps(element);
expect(props2).toEqual({
name: '@custom-api:icon-prefix:icon-name',
icon: {
provider: 'custom-api',
prefix: 'icon-prefix',
name: 'icon-name',
},
customisations: {
...defaults,
},
});
expect(propsChanged(props1, props2)).toBe(true);
// Short name
element.setAttribute('data-icon', 'mdi-home');
const props3 = getElementProps(element);
expect(props3).toEqual({
name: 'mdi-home',
icon: {
provider: '',
prefix: 'mdi',
name: 'home',
},
customisations: {
...defaults,
},
});
expect(propsChanged(props1, props3)).toBe(true);
expect(propsChanged(props2, props3)).toBe(true);
// Invalid name
element.setAttribute('data-icon', 'home');
expect(getElementProps(element)).toBeNull();
});
it('Inline icon', () => {
const element = document.createElement('span');
// Set icon name
const name = 'mdi:home';
const icon = {
provider: '',
prefix: 'mdi',
name: 'home',
};
element.setAttribute('data-icon', name);
// Block: default
const props1Block = getElementProps(element);
expect(props1Block).toEqual({
name,
icon,
customisations: {
...defaults,
inline: false,
},
});
// Inline: set via attribute
element.setAttribute('data-inline', 'true');
const props2Inline = getElementProps(element);
expect(props2Inline).toEqual({
name,
icon,
customisations: {
...defaults,
inline: true,
},
});
expect(propsChanged(props1Block, props2Inline)).toBe(true);
// Block: set via attribute
element.setAttribute('data-inline', 'false');
const props3Block = getElementProps(element);
expect(props3Block).toEqual({
name,
icon,
customisations: {
...defaults,
inline: false,
},
});
expect(propsChanged(props1Block, props3Block)).toBe(false);
// Inline: set via class
element.removeAttribute('data-inline');
element.className = 'iconify-inline';
const props4Inline = getElementProps(element);
expect(props4Inline).toEqual({
name,
icon,
customisations: {
...defaults,
inline: true,
},
});
expect(propsChanged(props1Block, props4Inline)).toBe(true);
expect(propsChanged(props2Inline, props4Inline)).toBe(false);
// Block: set via class
element.className = 'iconify';
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
inline: false,
},
});
// Inline: set via attribute, overriding class
element.className = 'iconify';
element.setAttribute('data-inline', 'true');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
inline: true,
},
});
// Block: set via attribute, overriding class
element.className = 'iconify-inline';
element.setAttribute('data-inline', 'false');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
inline: false,
},
});
});
it('Dimensions', () => {
const element = document.createElement('span');
// Set icon name
const name = 'mdi:home';
const icon = {
provider: '',
prefix: 'mdi',
name: 'home',
};
element.setAttribute('data-icon', name);
// Default
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
width: null,
height: null,
},
});
// Set width
element.setAttribute('data-width', '200');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
width: '200',
height: null,
},
});
// Set height
element.setAttribute('data-height', '1em');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
width: '200',
height: '1em',
},
});
// Empty width
element.setAttribute('data-width', '');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
width: null,
height: '1em',
},
});
});
it('Rotation', () => {
const element = document.createElement('span');
// Set icon name
const name = 'mdi:home';
const icon = {
provider: '',
prefix: 'mdi',
name: 'home',
};
element.setAttribute('data-icon', name);
// Default
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
rotate: 0,
},
});
// 90deg
element.setAttribute('data-rotate', '90deg');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
rotate: 1,
},
});
// 180deg
element.setAttribute('data-rotate', '2');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
rotate: 2,
},
});
// 270deg
element.setAttribute('data-rotate', '75%');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
rotate: 3,
},
});
// 270deg
element.setAttribute('data-rotate', '-90deg');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
rotate: 3,
},
});
// Invalid values or 0 deg
element.setAttribute('data-rotate', '45deg');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
},
});
element.setAttribute('data-rotate', '360deg');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
},
});
element.setAttribute('data-rotate', 'true');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
},
});
element.setAttribute('data-rotate', '-100%');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
},
});
});
it('Flip', () => {
const element = document.createElement('span');
// Set icon name
const name = 'mdi:home';
const icon = {
provider: '',
prefix: 'mdi',
name: 'home',
};
element.setAttribute('data-icon', name);
// Default
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
hFlip: false,
vFlip: false,
},
});
// Horizontal
element.setAttribute('data-flip', 'horizontal');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
hFlip: true,
},
});
// Both
element.setAttribute('data-vFlip', 'true');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
hFlip: true,
vFlip: true,
},
});
// Vertical
element.removeAttribute('data-vFlip');
element.setAttribute('data-flip', 'vertical');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
vFlip: true,
},
});
// Overwriting shorthand attr
element.setAttribute('data-vFlip', 'false');
element.setAttribute('data-flip', 'vertical');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
},
});
// Both
element.removeAttribute('data-vFlip');
element.setAttribute('data-flip', 'vertical,horizontal');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
hFlip: true,
vFlip: true,
},
});
// None
element.setAttribute('data-vFlip', 'false');
element.setAttribute('data-hFlip', 'false');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
},
});
});
it('Alignment', () => {
const element = document.createElement('span');
// Set icon name
const name = 'mdi:home';
const icon = {
provider: '',
prefix: 'mdi',
name: 'home',
};
element.setAttribute('data-icon', name);
// Default
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
hAlign: 'center',
vAlign: 'middle',
slice: false,
},
});
// Horizontal
element.setAttribute('data-align', 'left');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
hAlign: 'left',
},
});
element.setAttribute('data-align', 'right,meet');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
hAlign: 'right',
},
});
// Vertical, slice
element.setAttribute('data-align', 'center,top,slice');
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
vAlign: 'top',
slice: true,
},
});
// Overrides, spaces
element.setAttribute(
'data-align',
'left right top middle meet\t slice'
);
expect(getElementProps(element)).toEqual({
name,
icon,
customisations: {
...defaults,
hAlign: 'right',
slice: true,
},
});
});
});

View File

@ -0,0 +1,130 @@
import { JSDOM } from 'jsdom';
import { mockAPIModule, mockAPIData } from '@iconify/core/lib/api/modules/mock';
import { addAPIProvider } from '@iconify/core/lib/api/config';
import { setAPIModule } from '@iconify/core/lib/api/modules';
import { removeRootNode, listRootNodes } from '../src/observer/root';
import { onReady } from '../src/helpers/ready';
import { stopObserver, stopObserving } from '../src/observer';
/**
* Generate next prefix
*/
let counter = 0;
export const nextPrefix = () => 'mock-' + counter++;
/**
* Set mock API module for provider
*/
export function fakeAPI(provider: string) {
// Set API module for provider
addAPIProvider(provider, {
resources: ['http://localhost'],
});
setAPIModule(provider, mockAPIModule);
}
export { mockAPIData };
/**
* Async version of onReady()
*/
export function waitDOMReady() {
return new Promise((fulfill) => {
onReady(() => {
fulfill(undefined);
});
});
}
/**
* Timeout
*
* Can chain multiple setTimeout by adding multiple 0 delays
*/
export function nextTick(delays: number[] = [0]) {
return new Promise((fulfill) => {
function next() {
if (!delays.length) {
fulfill(undefined);
return;
}
setTimeout(() => {
next();
}, delays.shift());
}
next();
});
}
/**
* Timeout, until condition is met
*/
type WaitUntilCheck = () => boolean;
export function awaitUntil(callback: WaitUntilCheck, maxDelay = 1000) {
return new Promise((fulfill, reject) => {
const start = Date.now();
function next() {
setTimeout(() => {
if (callback()) {
fulfill(undefined);
return;
}
if (Date.now() - start > maxDelay) {
reject('Timed out');
return;
}
next();
});
}
next();
});
}
/**
* Create JSDOM instance, overwrite globals
*/
export function setupDOM(html: string): JSDOM {
const dom = new JSDOM(html);
(global as unknown as Record<string, unknown>).window = dom.window;
(global as unknown as Record<string, unknown>).document =
global.window.document;
return dom;
}
/**
* Delete temporary globals
*/
export function cleanupGlobals() {
delete global.window;
delete global.document;
}
/**
* Reset state
*/
export function resetState() {
// Reset root nodes list and stop all observers
listRootNodes().forEach((node) => {
stopObserver(node);
removeRootNode(node);
});
// Remove globals
cleanupGlobals();
}
/**
* Wrap HTML
*/
export function wrapHTML(content: string): string {
return `<!doctype html>
<head>
<meta charset="utf-8">
</head>
<body>
${content}
</body>
</html>`;
}

View File

@ -1,53 +0,0 @@
import Iconify, { IconifyIconName } from '../';
import { mockAPIModule, mockAPIData } from '@iconify/core/lib/api/modules/mock';
// API provider and prefix for test
const provider = 'mock-api';
const prefix = 'prefix';
// Set API module for provider
Iconify.addAPIProvider(provider, {
resources: ['http://localhost'],
});
Iconify._api.setAPIModule(provider, mockAPIModule);
// Set data
mockAPIData({
type: 'icons',
provider,
prefix,
response: {
prefix,
icons: {
home: {
body: '<path d="M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8h5z" fill="currentColor"/>',
},
},
width: 24,
height: 24,
},
});
// Test
describe('Testing loadIcons() with Node.js', () => {
it('Load icons from API', (done) => {
const name = 'home';
const fullName = '@' + provider + ':' + prefix + ':' + name;
Iconify.loadIcons([fullName], (loaded, missing) => {
// Check callback data
expect(missing).toEqual([]);
const icon: IconifyIconName = {
provider,
prefix,
name,
};
expect(loaded).toEqual([icon]);
// Check if icon exists
expect(Iconify.iconExists(fullName)).toBe(true);
done();
});
});
});

View File

@ -0,0 +1,41 @@
import { iconExists } from '@iconify/core/lib/storage/functions';
import { loadIcon } from '@iconify/core/lib/api/icons';
import { iconDefaults } from '@iconify/utils/lib/icon';
import { fakeAPI, nextPrefix, mockAPIData } from './helpers';
describe('Testing mock API', () => {
it('Setting up API', async () => {
// Set config
const provider = nextPrefix();
const prefix = nextPrefix();
fakeAPI(provider);
// Mock data
const name = 'mock-test';
const iconName = `@${provider}:${prefix}:${name}`;
mockAPIData({
type: 'icons',
provider,
prefix,
response: {
prefix,
icons: {
[name]: {
body: '<g />',
},
},
},
});
// Check if icon has been loaded
expect(iconExists(iconName)).toBe(false);
// Load icon
const data = await loadIcon(iconName);
expect(data).toEqual({
...iconDefaults,
body: '<g />',
});
});
});

View File

@ -1,12 +0,0 @@
import Iconify from '../';
describe('Testing Iconify observer functions with Node.js', () => {
it('Observer functions', () => {
// All functions should fail, not without throwing exceptions
expect(Iconify.scan()).toBeUndefined();
expect(Iconify.pauseObserver()).toBeUndefined();
expect(Iconify.resumeObserver()).toBeUndefined();
// Cannot test observe() and stopObserving() because they require DOM node as parameter
});
});

View File

@ -0,0 +1,297 @@
import { iconExists } from '@iconify/core/lib/storage/functions';
import {
fakeAPI,
nextPrefix,
setupDOM,
waitDOMReady,
resetState,
mockAPIData,
awaitUntil,
nextTick,
} from './helpers';
import { addBodyNode } from '../src/observer/root';
import { scanDOM } from '../src/scanner/index';
import { elementDataProperty } from '../src/scanner/config';
import { initObserver } from '../src/observer';
describe('Observing DOM changes', () => {
const provider = nextPrefix();
beforeAll(() => {
fakeAPI(provider);
});
afterEach(resetState);
it('Loading icon, transforming icon', async () => {
const prefix = nextPrefix();
const iconName = `@${provider}:${prefix}:home`;
// Add icon with API
expect(iconExists(iconName)).toBe(false);
mockAPIData({
type: 'icons',
provider,
prefix,
response: {
prefix,
icons: {
home: {
body: '<g />',
},
},
},
});
// Setup DOM and wait for it to be ready
setupDOM(`<span class="iconify" data-icon="${iconName}"></span>`);
await waitDOMReady();
// Observe body
addBodyNode();
initObserver(scanDOM);
// Check HTML and data
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
// Wait for re-render
const placeholder = document.body.childNodes[0];
await awaitUntil(() => document.body.childNodes[0] !== placeholder);
// Check HTML
expect(document.body.innerHTML).toBe(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" data-icon="${iconName}" class="iconify iconify--${provider} iconify--${prefix}"><g></g></svg>`
);
const svg = document.body.childNodes[0] as SVGSVGElement;
const svgData = svg[elementDataProperty];
expect(svgData.status).toBe('loaded');
expect(svgData.isSVG).toBe(true);
expect(svgData.name).toEqual(iconName);
// Rotate icon
svg.setAttribute('data-rotate', '90deg');
// Wait for re-render
await awaitUntil(
() => document.body.innerHTML.indexOf('transform=') !== -1
);
expect(document.body.innerHTML).toBe(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" data-icon="${iconName}" data-rotate="90deg" class="iconify iconify--${provider} iconify--${prefix}"><g transform="rotate(90 8 8)"><g></g></g></svg>`
);
});
it('Changing icon name after rendering first icon', async () => {
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
const iconName = `@${provider}:${prefix1}:home`;
const iconName2 = `@${provider}:${prefix2}:arrow`;
let sendSecondIcon = null;
// Add icon with API
expect(iconExists(iconName)).toBe(false);
expect(iconExists(iconName2)).toBe(false);
mockAPIData({
type: 'icons',
provider,
prefix: prefix1,
response: {
prefix: prefix1,
icons: {
home: {
body: '<g />',
},
},
},
});
mockAPIData({
type: 'icons',
provider,
prefix: prefix2,
response: {
prefix: prefix2,
icons: {
arrow: {
body: '<path d="M0 0v2" />',
width: 24,
height: 24,
},
},
},
delay: (send) => {
sendSecondIcon = send;
},
});
// Setup DOM and wait for it to be ready
setupDOM(`<span class="iconify" data-icon="${iconName}"></span>`);
await waitDOMReady();
// Observe body
addBodyNode();
initObserver(scanDOM);
// Check HTML and data
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
// Wait for re-render
const placeholder = document.body.childNodes[0];
await awaitUntil(() => document.body.childNodes[0] !== placeholder);
// Check HTML
expect(document.body.innerHTML).toBe(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" data-icon="${iconName}" class="iconify iconify--${provider} iconify--${prefix1}"><g></g></svg>`
);
const svg = document.body.childNodes[0] as SVGSVGElement;
const svgData = svg[elementDataProperty];
expect(svgData.status).toBe('loaded');
expect(svgData.isSVG).toBe(true);
expect(svgData.name).toEqual(iconName);
// Chang icon name
svg.setAttribute('data-icon', iconName2);
// Wait for DOM to be scanned again and API query to be sent
await awaitUntil(() => sendSecondIcon !== null);
// SVG should not have been replaced yet, but data should match new icon
expect(document.body.innerHTML).toBe(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" data-icon="${iconName2}" class="iconify iconify--${provider} iconify--${prefix1}"><g></g></svg>`
);
expect(document.body.childNodes[0]).toBe(svg);
expect(svgData.status).toBe('loading');
expect(svgData.isSVG).toBe(true);
expect(svgData.name).toEqual(iconName2);
// Send API query
sendSecondIcon();
// Wait for re-render
await awaitUntil(
() => document.body.innerHTML.indexOf('M0 0v2') !== -1
);
expect(document.body.innerHTML).toBe(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="${iconName2}" class="iconify iconify--${provider} iconify--${prefix2}"><path d="M0 0v2"></path></svg>`
);
});
it('Changing icon name while loading first icon', async () => {
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
const iconName = `@${provider}:${prefix1}:home`;
const iconName2 = `@${provider}:${prefix2}:arrow`;
let sendFirstIcon = null;
let sendSecondIcon = null;
// Add icon with API
expect(iconExists(iconName)).toBe(false);
expect(iconExists(iconName2)).toBe(false);
mockAPIData({
type: 'icons',
provider,
prefix: prefix1,
response: {
prefix: prefix1,
icons: {
home: {
body: '<g />',
},
},
},
delay: (send) => {
sendFirstIcon = send;
},
});
mockAPIData({
type: 'icons',
provider,
prefix: prefix2,
response: {
prefix: prefix2,
icons: {
arrow: {
body: '<path d="M0 0v2" />',
width: 24,
height: 24,
},
},
},
delay: (send) => {
sendSecondIcon = send;
},
});
// Setup DOM and wait for it to be ready
setupDOM(`<span class="iconify" data-icon="${iconName}"></span>`);
await waitDOMReady();
// Observe body
addBodyNode();
initObserver(scanDOM);
// Check HTML and data
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
// Wait for DOM to be scanned again and API query to be sent
await awaitUntil(() => sendFirstIcon !== null);
// Check HTML
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
const placeholder = document.body.childNodes[0] as HTMLSpanElement;
const placeholderData = placeholder[elementDataProperty];
expect(placeholderData.status).toBe('loading');
expect(placeholderData.isSVG).toBeFalsy();
expect(placeholderData.name).toEqual(iconName);
// Chang icon name
placeholder.setAttribute('data-icon', iconName2);
// Wait for DOM to be scanned again and API query to be sent
await awaitUntil(() => sendSecondIcon !== null);
// SVG should not have been rendered yet, but data should match new icon
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName2}"></span>`
);
expect(document.body.childNodes[0]).toBe(placeholder);
expect(placeholderData.status).toBe('loading');
expect(placeholderData.isSVG).toBeFalsy();
expect(placeholderData.name).toEqual(iconName2);
// Send first API query
sendFirstIcon();
await awaitUntil(() => iconExists(iconName));
// Wait a bit more
await nextTick([0, 0, 0]);
// Nothing should have changed
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName2}"></span>`
);
expect(document.body.childNodes[0]).toBe(placeholder);
expect(placeholderData.status).toBe('loading');
expect(placeholderData.isSVG).toBeFalsy();
expect(placeholderData.name).toEqual(iconName2);
// Send second API query
sendSecondIcon();
// Wait for re-render
await awaitUntil(
() => document.body.innerHTML.indexOf('M0 0v2') !== -1
);
expect(document.body.innerHTML).toBe(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="${iconName2}" class="iconify iconify--${provider} iconify--${prefix2}"><path d="M0 0v2"></path></svg>`
);
});
});

View File

@ -0,0 +1,240 @@
import { iconDefaults } from '@iconify/utils/lib/icon';
import { cleanupGlobals, setupDOM, waitDOMReady } from './helpers';
import { scanRootNode } from '../src/scanner/find';
import { renderInlineSVG } from '../src/render/svg';
import type { IconifyIcon } from '@iconify/utils/lib/icon';
import { elementDataProperty, IconifyElement } from '../src/scanner/config';
describe('Testing re-rendering nodes', () => {
afterEach(cleanupGlobals);
type TestIconCallback = (svg: IconifyElement) => IconifyIcon;
const testIcon = async (
placeholder: string,
data: IconifyIcon,
expected1: string,
callback1: TestIconCallback,
expected2: string,
callback2?: TestIconCallback,
expected3?: string
): Promise<IconifyElement> => {
setupDOM(placeholder);
await waitDOMReady();
function scan(expected: string): IconifyElement {
// Find node
const root = document.body;
const items = scanRootNode(root);
expect(items.length).toBe(1);
// Get node and render it
const { node, props } = items[0];
const svg = renderInlineSVG(node, props, {
...iconDefaults,
...data,
});
// Find SVG in DOM
expect(root.childNodes.length).toBe(1);
const expectedSVG = root.childNodes[0];
expect(expectedSVG).toBe(svg);
// Get HTML
const html = root.innerHTML;
expect(html).toBe(expected);
return svg;
}
// Initial scan
const svg1 = scan(expected1);
// Change element
data = callback1(svg1);
// Scan again
const svg2 = scan(expected2);
if (!callback2) {
return svg2;
}
// Change element again
data = callback2(svg2);
// Scan DOM and return result
return scan(expected3);
};
it('Changing content', async () => {
const svg = await testIcon(
'<span class="iconify" data-icon="mdi:home"></span>',
{
body: '<g />',
width: 24,
height: 24,
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" class="iconify iconify--mdi"><g></g></svg>',
(svg) => {
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
// Change icon name and size
svg.setAttribute('data-icon', 'mdi-light:home-outline');
svg.setAttribute('data-height', 'auto');
return {
body: '<path d="" />',
width: 32,
height: 32,
};
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32" data-icon="mdi-light:home-outline" data-height="auto" class="iconify iconify--mdi-light"><path d=""></path></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi-light']);
expect(data.addedStyles).toEqual([]);
});
it('Toggle inline and block using class', async () => {
const iconData: IconifyIcon = {
body: '<g />',
width: 24,
height: 24,
};
const svg = await testIcon(
'<span class="iconify" data-icon="mdi:home"></span>',
iconData,
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" class="iconify iconify--mdi"><g></g></svg>',
(svg) => {
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
// Set inline by adding class
svg.classList.add('iconify-inline');
return iconData;
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" class="iconify iconify--mdi iconify-inline" style="vertical-align: -0.125em;"><g></g></svg>',
(svg) => {
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual(['vertical-align']);
// Set block by removing class
svg.classList.remove('iconify-inline');
return iconData;
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" style="" class="iconify iconify--mdi"><g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
});
it('Toggle inline and block using attributes', async () => {
const iconData: IconifyIcon = {
body: '<g />',
width: 24,
height: 24,
};
const svg = await testIcon(
'<span class="iconify" data-icon="mdi:home"></span>',
iconData,
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" class="iconify iconify--mdi"><g></g></svg>',
(svg) => {
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
// Set inline by adding attribute
svg.setAttribute('data-inline', 'data-inline');
return iconData;
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" data-inline="data-inline" class="iconify iconify--mdi" style="vertical-align: -0.125em;"><g></g></svg>',
(svg) => {
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual(['vertical-align']);
// Set block by setting empty attribute
svg.setAttribute('data-inline', '');
return iconData;
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" data-inline="" style="" class="iconify iconify--mdi"><g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
});
it('Transformations', async () => {
const svg = await testIcon(
'<span class="iconify" data-icon="mdi:home" data-flip="horizontal"></span>',
{
body: '<g />',
width: 24,
height: 24,
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" data-flip="horizontal" class="iconify iconify--mdi"><g transform="translate(24 0) scale(-1 1)"><g></g></g></svg>',
(svg) => {
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
// Rotate and flip
svg.setAttribute('data-rotate', '90deg');
svg.setAttribute('data-flip', 'vertical');
return {
body: '<path d="" />',
width: 32,
height: 32,
};
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32" data-icon="mdi:home" data-flip="vertical" data-rotate="90deg" class="iconify iconify--mdi"><g transform="rotate(90 16 16) translate(0 32) scale(1 -1)"><path d=""></path></g></svg>',
(svg) => {
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
// Rotate and remove flip
svg.setAttribute('data-rotate', '180deg');
svg.removeAttribute('data-flip');
return {
body: '<g />',
};
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" data-icon="mdi:home" data-rotate="180deg" class="iconify iconify--mdi"><g transform="rotate(180 8 8)"><g></g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
});
});

View File

@ -0,0 +1,25 @@
import { cleanupGlobals, setupDOM } from './helpers';
import { onReady } from '../src/helpers/ready';
describe('Testing onReady callback', () => {
afterEach(cleanupGlobals);
it('Testing onReady before DOM is loaded', (done) => {
setupDOM('');
expect(document.readyState).toBe('loading');
onReady(() => {
done();
});
});
it('Testing onReady after DOM is loaded', (done) => {
setupDOM('');
expect(document.readyState).toBe('loading');
document.addEventListener('DOMContentLoaded', () => {
expect(document.readyState).toBe('interactive');
onReady(() => {
done();
});
});
});
});

View File

@ -0,0 +1,147 @@
import { iconDefaults } from '@iconify/utils/lib/icon';
import { cleanupGlobals, setupDOM, waitDOMReady } from './helpers';
import { scanRootNode } from '../src/scanner/find';
import { renderInlineSVG } from '../src/render/svg';
import type { IconifyIcon } from '@iconify/utils/lib/icon';
import { elementDataProperty, IconifyElement } from '../src/scanner/config';
describe('Testing rendering nodes', () => {
afterEach(cleanupGlobals);
const testIcon = async (
placeholder: string,
data: IconifyIcon,
expected: string
): Promise<IconifyElement> => {
setupDOM(placeholder);
await waitDOMReady();
// Find node
const root = document.body;
const items = scanRootNode(root);
expect(items.length).toBe(1);
// Get node and render it
const { node, props } = items[0];
const svg = renderInlineSVG(node, props, { ...iconDefaults, ...data });
// Find SVG in DOM
expect(root.childNodes.length).toBe(1);
const expectedSVG = root.childNodes[0];
expect(expectedSVG).toBe(svg);
// Get HTML
const html = root.innerHTML;
expect(html).toBe(expected);
return svg;
};
it('Rendering simple icon', async () => {
const svg = await testIcon(
'<span class="iconify" data-icon="mdi:home"></span>',
{
body: '<g />',
width: 24,
height: 24,
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" class="iconify iconify--mdi"><g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--mdi']);
expect(data.addedStyles).toEqual([]);
});
it('Inline icon and transformation', async () => {
const svg = await testIcon(
'<i class="iconify-inline" data-icon="mdi:home" data-flip="horizontal"></i>',
{
body: '<g />',
width: 24,
height: 24,
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" data-flip="horizontal" class="iconify-inline iconify iconify--mdi" style="vertical-align: -0.125em;"><g transform="translate(24 0) scale(-1 1)"><g></g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify', 'iconify--mdi']);
expect(data.addedStyles).toEqual(['vertical-align']);
});
it('Passing attributes and style', async () => {
const svg = await testIcon(
'<span id="test" style="color: red; vertical-align: -0.1em;" class="iconify my-icon iconify--mdi" data-icon="mdi:home"></span>',
{
body: '<g />',
width: 24,
height: 24,
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" id="test" style="color: red; vertical-align: -0.1em;" data-icon="mdi:home" class="iconify my-icon iconify--mdi"><g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual([]); // All classes already existed on placeholder
expect(data.addedStyles).toEqual([]); // Overwritten by entry in placeholder
});
it('Inline icon and vertical-align', async () => {
const svg = await testIcon(
'<i class="iconify-inline" data-icon="mdi:home" style="vertical-align: 0"></i>',
{
body: '<g />',
width: 24,
height: 24,
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="mdi:home" style="vertical-align: 0" class="iconify-inline iconify iconify--mdi"><g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify', 'iconify--mdi']);
expect(data.addedStyles).toEqual([]);
});
it('Inline icon and custom style without ;', async () => {
const svg = await testIcon(
'<i class="iconify-inline" data-icon="@provider:mdi-light:home-outline" style="color: red"></i>',
{
body: '<g />',
width: 24,
height: 24,
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="@provider:mdi-light:home-outline" style="color: red; vertical-align: -0.125em;" class="iconify-inline iconify iconify--provider iconify--mdi-light"><g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual([
'iconify',
'iconify--provider',
'iconify--mdi-light',
]);
expect(data.addedStyles).toEqual(['vertical-align']);
});
it('Identical prefix and provider', async () => {
const svg = await testIcon(
'<i class="iconify" data-icon="@test:test:arrow-left"></i>',
{
body: '<g />',
width: 24,
height: 24,
},
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" data-icon="@test:test:arrow-left" class="iconify iconify--test"><g></g></svg>'
);
const data = svg[elementDataProperty];
expect(data.status).toBe('loaded');
expect(data.addedClasses).toEqual(['iconify--test']);
expect(data.addedStyles).toEqual([]);
});
});

View File

@ -1,33 +0,0 @@
import Iconify, { IconifyIconBuildResult } from '../';
describe('Testing Iconify render functions with Node.js', () => {
const prefix = 'node-test-render';
const name = prefix + ':icon';
it('Render functions', () => {
// Add icon
expect(
Iconify.addIcon(name, {
body: '<g />',
width: 24,
height: 24,
})
).toBe(true);
// renderIcon() should work
const expected: IconifyIconBuildResult = {
attributes: {
width: '1em',
height: '1em',
preserveAspectRatio: 'xMidYMid meet',
viewBox: '0 0 24 24',
},
body: '<g />',
};
expect(Iconify.renderIcon(name, {})).toEqual(expected);
// renderHTML() and renderSVG() should fail because document.createElement does not exist
expect(Iconify.renderHTML(name, {})).toBe('');
expect(Iconify.renderSVG(name, {})).toBeNull();
});
});

View File

@ -0,0 +1,107 @@
import { resetState, setupDOM, wrapHTML } from './helpers';
import {
listRootNodes,
addBodyNode,
addRootNode,
removeRootNode,
} from '../src/observer/root';
describe('Testing root nodes', () => {
afterEach(resetState);
it('Testing body element', () => {
setupDOM('');
expect(document.readyState).toBe('loading');
// Add body node
addBodyNode();
// List should have document.body
expect(listRootNodes()).toEqual([
{
node: document.documentElement,
temporary: false,
},
]);
});
it('Adding and removing nodes', () => {
setupDOM(
wrapHTML('<div id="root-test"></div><div id="root-test2"></div>')
);
expect(document.readyState).toBe('loading');
// Get test nodes, make sure they exist
const node1 = document.getElementById('root-test');
expect(node1.tagName).toBe('DIV');
expect(node1.getAttribute('id')).toBe('root-test');
const node2 = document.getElementById('root-test2');
expect(node2.tagName).toBe('DIV');
expect(node2.getAttribute('id')).toBe('root-test2');
// Add body node and temporary nodes
addBodyNode();
addRootNode(node1);
addRootNode(node2, true);
// List nodes
expect(listRootNodes()).toEqual([
{
node: document.documentElement,
temporary: false,
},
{
node: node1,
temporary: false,
},
{
node: node2,
temporary: true,
},
]);
// Switch type for node2
addRootNode(node2);
expect(listRootNodes()).toEqual([
{
node: document.documentElement,
temporary: false,
},
{
node: node1,
temporary: false,
},
{
node: node2,
temporary: false,
},
]);
// Remove node2
removeRootNode(node2);
expect(listRootNodes()).toEqual([
{
node: document.documentElement,
temporary: false,
},
{
node: node1,
temporary: false,
},
]);
// Add duplicate node1
addRootNode(node1, true);
expect(listRootNodes()).toEqual([
{
node: document.documentElement,
temporary: false,
},
{
node: node1,
temporary: false,
},
]);
});
});

View File

@ -0,0 +1,175 @@
import { iconExists, addIcon } from '@iconify/core/lib/storage/functions';
import {
fakeAPI,
nextPrefix,
setupDOM,
waitDOMReady,
resetState,
mockAPIData,
awaitUntil,
} from './helpers';
import { addBodyNode } from '../src/observer/root';
import { scanDOM } from '../src/scanner/index';
import { elementDataProperty } from '../src/scanner/config';
describe('Scanning DOM', () => {
const provider = nextPrefix();
beforeAll(() => {
fakeAPI(provider);
});
afterEach(resetState);
it('Rendering preloaded icon', async () => {
const prefix = nextPrefix();
const iconName = `@${provider}:${prefix}:home`;
// Add icon
expect(iconExists(iconName)).toBe(false);
addIcon(iconName, {
body: '<g />',
});
expect(iconExists(iconName)).toBe(true);
// Setup DOM and wait for it to be ready
setupDOM(`<span class="iconify" data-icon="${iconName}"></span>`);
await waitDOMReady();
// Observe body
addBodyNode();
// Check HTML
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
const placeholder = document.body.childNodes[0];
expect(placeholder[elementDataProperty]).toBeUndefined();
// Scan DOM
scanDOM();
// Check HTML
expect(document.body.innerHTML).toBe(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" data-icon="${iconName}" class="iconify iconify--${provider} iconify--${prefix}"><g></g></svg>`
);
const svg = document.body.childNodes[0];
const svgData = svg[elementDataProperty];
expect(svgData.status).toBe('loaded');
expect(svgData.name).toEqual(iconName);
});
it('Loading icon', async () => {
const prefix = nextPrefix();
const iconName = `@${provider}:${prefix}:home`;
// Add icon with API
expect(iconExists(iconName)).toBe(false);
mockAPIData({
type: 'icons',
provider,
prefix,
response: {
prefix,
icons: {
home: {
body: '<g />',
},
},
},
});
// Setup DOM and wait for it to be ready
setupDOM(`<span class="iconify" data-icon="${iconName}"></span>`);
await waitDOMReady();
// Observe body
addBodyNode();
// Check HTML and data
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
const placeholder = document.body.childNodes[0];
expect(placeholder[elementDataProperty]).toBeUndefined();
// Scan DOM
scanDOM();
// Check HTML again
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
expect(placeholder).toBe(document.body.childNodes[0]);
const placeholderData = placeholder[elementDataProperty];
expect(placeholderData.status).toBe('loading');
expect(placeholderData.name).toEqual(iconName);
expect(placeholderData.isSVG).toBeFalsy();
// Wait for re-render
await awaitUntil(() => document.body.childNodes[0] !== placeholder);
// Check HTML
expect(document.body.innerHTML).toBe(
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" data-icon="${iconName}" class="iconify iconify--${provider} iconify--${prefix}"><g></g></svg>`
);
const svg = document.body.childNodes[0];
const svgData = svg[elementDataProperty];
expect(svgData.status).toBe('loaded');
expect(svgData.isSVG).toBe(true);
expect(svgData.name).toEqual(iconName);
});
it('Missing icon', async () => {
const prefix = nextPrefix();
const iconName = `@${provider}:${prefix}:home`;
// Add icon with API
expect(iconExists(iconName)).toBe(false);
mockAPIData({
type: 'icons',
provider,
prefix,
response: {
prefix,
icons: {},
not_found: ['home'],
},
});
// Setup DOM and wait for it to be ready
setupDOM(`<span class="iconify" data-icon="${iconName}"></span>`);
await waitDOMReady();
// Observe body
addBodyNode();
// Check HTML and data
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
const placeholder = document.body.childNodes[0];
expect(placeholder[elementDataProperty]).toBeUndefined();
// Scan DOM
scanDOM();
// Check HTML again
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
expect(placeholder).toBe(document.body.childNodes[0]);
const placeholderData = placeholder[elementDataProperty];
expect(placeholderData.status).toBe('loading');
expect(placeholderData.name).toEqual(iconName);
expect(placeholderData.isSVG).toBeFalsy();
// Wait for re-render
await awaitUntil(() => placeholderData.status === 'missing');
// Check HTML
expect(document.body.innerHTML).toBe(
`<span class="iconify" data-icon="${iconName}"></span>`
);
});
});

View File

@ -0,0 +1,62 @@
import { cleanupGlobals, setupDOM, waitDOMReady } from './helpers';
import { scanRootNode } from '../src/scanner/find';
import { getElementProps } from '../src/scanner/get-props';
describe('Testing scanning nodes', () => {
afterEach(cleanupGlobals);
it('Finding basic placeholders', async () => {
setupDOM(`
<span class="iconify" data-icon="mdi:home"></span>
<span class="iconify-inline" data-icon="mdi-light:home"></span>
<p>
<i class="iconify" data-icon="mdi:home-outline"></i>
<i class="iconify-inline" data-icon="ic:baseline-home"></i>
</p>
`);
await waitDOMReady();
const root = document.documentElement;
const items = scanRootNode(root);
// 4 nodes
expect(items.length).toBe(4);
// span.iconify
const node0 = root.querySelector('span.iconify');
expect(items[0].node).toEqual(node0);
expect(items[0].props).toEqual(getElementProps(node0));
// span.iconify-inline
const node1 = root.querySelector('span.iconify-inline');
expect(items[1].node).toEqual(node1);
expect(items[1].props).toEqual(getElementProps(node1));
// i.iconify
const node2 = root.querySelector('i.iconify');
expect(items[2].node).toEqual(node2);
expect(items[2].props).toEqual(getElementProps(node2));
// i.iconify-inline
const node3 = root.querySelector('i.iconify-inline');
expect(items[3].node).toEqual(node3);
expect(items[3].props).toEqual(getElementProps(node3));
});
it('Invalid placeholders', async () => {
setupDOM(`
<span class="iconify" data-icon="badicon"></span>
<span data-icon="prefix:missing-class"></span>
<strong class="iconify" data-icon="prefix:invalid-tag"></strong>
<svg class="iconify iconify--prefix" data-icon="prefix:svg-without-data"></svg>
`);
await waitDOMReady();
const root = document.documentElement;
const items = scanRootNode(root);
expect(items.length).toBe(0);
});
});

View File

@ -1,75 +0,0 @@
import { readFileSync } from 'fs';
import { dirname } from 'path';
import Iconify, { IconifyIcon } from '../';
describe('Testing Iconify with Node.js', () => {
it('Basic functions', () => {
expect(typeof Iconify).toBe('object');
// Placeholder value should have been replaced during compilation
const version = JSON.parse(
readFileSync(dirname(__dirname) + '/package.json', 'utf8')
).version;
expect(Iconify.getVersion()).toBe(version);
});
it('Builder functions', () => {
// calculateSize() should work in Node.js
expect(Iconify.calculateSize('24px', 2)).toBe('48px');
// replaceIDs() should work in Node.js
const test = '<div id="foo" />';
expect(Iconify.replaceIDs(test)).not.toBe(test);
});
it('Storage functions', () => {
const prefix = 'node-test-storage';
const name = prefix + ':bar';
// Empty results
expect(Iconify.iconExists(name)).toBe(false);
expect(Iconify.getIcon(name)).toBeNull();
expect(Iconify.listIcons('', prefix)).toEqual([]);
// Test addIcon()
expect(
Iconify.addIcon(name, {
body: '<g />',
width: 24,
height: 24,
})
).toBe(true);
expect(Iconify.iconExists(name)).toBe(true);
const expected: Required<IconifyIcon> = {
body: '<g />',
width: 24,
height: 24,
left: 0,
top: 0,
hFlip: false,
vFlip: false,
rotate: 0,
};
expect(Iconify.getIcon(name)).toEqual(expected);
expect(Iconify.listIcons('', prefix)).toEqual([name]);
// Test addCollection()
expect(
Iconify.addCollection({
prefix,
icons: {
test1: {
body: '<g />',
},
},
width: 24,
height: 24,
})
).toBe(true);
expect(Iconify.listIcons('', prefix)).toEqual([
name,
prefix + ':test1',
]);
});
});