diff --git a/packages/core/tests/30-api/30-mock-test.ts b/packages/core/tests/30-api/30-mock-test.ts index 49d6246..b23c154 100644 --- a/packages/core/tests/30-api/30-mock-test.ts +++ b/packages/core/tests/30-api/30-mock-test.ts @@ -7,7 +7,7 @@ import { setAPIModule } from '../../lib/api/modules'; import { API } from '../../lib/api/'; import type { IconifyMockAPIDelayDoneCallback } from '../../lib/api/modules/mock'; import { mockAPIModule, mockAPIData } from '../../lib/api/modules/mock'; -import { allowSimpleNames } from '../../lib/storage/functions'; +import { getStorage, iconExists } from '../../lib/storage/storage'; describe('Testing mock API module', () => { let prefixCounter = 0; @@ -238,4 +238,46 @@ describe('Testing mock API module', () => { } ); }); + + // This is useful for testing component where loadIcons() cannot be accessed + it('Using timer in callback for second test', (done) => { + const prefix = nextPrefix(); + const name = 'test1'; + + // Mock data + mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + [name]: { + body: '', + }, + }, + }, + delay: (next) => { + // Icon should not be loaded yet + const storage = getStorage(provider, prefix); + expect(iconExists(storage, name)).to.be.equal(false); + + // Set data + next(); + + // Icon should be loaded now + expect(iconExists(storage, name)).to.be.equal(true); + + done(); + }, + }); + + // Load icons + API.loadIcons([ + { + provider, + prefix, + name, + }, + ]); + }); }); diff --git a/packages/react-demo/package.json b/packages/react-demo/package.json index 72b572d..6e2e784 100644 --- a/packages/react-demo/package.json +++ b/packages/react-demo/package.json @@ -30,6 +30,7 @@ "devDependencies": { "@iconify-icons/mdi-light": "^1.1.0", "@iconify-icons/uil": "^1.1.1", + "@iconify/core": "^1.0.0-rc.4", "@iconify/react": "^3.0.0-dev" } } diff --git a/packages/react-demo/src/App.js b/packages/react-demo/src/App.js index a263c4a..5fad717 100644 --- a/packages/react-demo/src/App.js +++ b/packages/react-demo/src/App.js @@ -6,6 +6,7 @@ import { import { addIcon as addOnlineIcon, addCollection as addOnlineCollection, + disableCache, } from '@iconify/react/dist/iconify'; import presentationPlay from '@iconify-icons/mdi-light/presentation-play'; import playIcon from '@iconify-icons/mdi-light/play'; @@ -14,11 +15,16 @@ import { Checkbox } from './demo-components/Checkbox'; import { InlineDemo } from './demo-components/Inline'; import { OfflineUsageDemo } from './demo-components/UsageOffline'; import { FullOfflineUsageDemo } from './demo-components/UsageFullOffline'; +import { FullUsageDemo } from './demo-components/UsageFull'; import { TestsOffline } from './test-components/TestsOffline'; import { TestsFullOffline } from './test-components/TestsFullOffline'; +import { TestsFull } from './test-components/TestsFull'; import './App.css'; +// Disable cache +disableCache('all'); + // Add 'mdi-light:presentation-play' as 'demo' for offline module addOfflineIcon('demo', presentationPlay); @@ -76,6 +82,7 @@ function App() {
+

Checkbox

@@ -97,6 +104,7 @@ function App() { +
); } diff --git a/packages/react-demo/src/demo-components/UsageFull.jsx b/packages/react-demo/src/demo-components/UsageFull.jsx new file mode 100644 index 0000000..cc9020e --- /dev/null +++ b/packages/react-demo/src/demo-components/UsageFull.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Icon } from '@iconify/react/dist/iconify'; + +export function FullUsageDemo() { + return ( +
+

Usage (full module)

+
+ Icon referenced by name: +
+
+ + Important notice with alert icon! +
+
+ ); +} diff --git a/packages/react-demo/src/test-components/TestsFull.jsx b/packages/react-demo/src/test-components/TestsFull.jsx new file mode 100644 index 0000000..ddffcc9 --- /dev/null +++ b/packages/react-demo/src/test-components/TestsFull.jsx @@ -0,0 +1,243 @@ +import React from 'react'; +import { InlineIcon, addAPIProvider, _api } from '@iconify/react/dist/iconify'; +import { mockAPIModule, mockAPIData } from '@iconify/core/lib/api/modules/mock'; +import { TestIcons, toggleTest } from './TestIcons'; +import playIcon from '@iconify-icons/mdi-light/map-marker'; + +// API provider for tests +const provider = 'mock-api'; +const prefix = 'demo'; + +// Set API module for provider +addAPIProvider(provider, { + resources: 'http://localhost', + rotate: 10000, + timeout: 10000, +}); +_api.setAPIModule(provider, mockAPIModule); + +// Set mock data +mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + icon: playIcon, + }, + }, + delay: 2000, +}); + +export function TestsFull() { + const icon = `@${provider}:${prefix}:icon`; + + return ( +
+

Tests (full module, with API)

+ +

References

+ +

Icons should load 2 seconds after page load

+ +
+ + Getting reference + { + const key = 'full-ref1'; + if (element && element.tagName === 'svg') { + toggleTest(key, 'success'); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Getting reference for empty icon + { + // Cannot be called because there is no SVG to render! + toggleTest('full-ref-missing', 'failed'); + }} + /> +
+ +
+ + Getting reference for missing icon with fallback text{' '} + { + // Cannot be called because there is no SVG to render! + toggleTest('full-ref-missing2', 'failed'); + }} + > + 😀 + +
+ +

Style

+ +
+ + Inline style for icon + { + const key = 'full-style'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(23, 105, 170)': + case '#1769aa': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + if (style.fontSize !== '24px') { + console.log( + 'Invalid font-size:', + style.fontSize + ); + errors = true; + } + + if (style.verticalAlign !== '-0.25em') { + console.log( + 'Invalid vertical-align:', + style.verticalAlign + ); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from attribute:{' '} + { + const key = 'full-color1'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from style:{' '} + { + const key = 'full-color2'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+ +
+ + Green color from attribute (overrides style) + red from style:{' '} + { + const key = 'full-color3'; + if (element && element.tagName === 'svg') { + let errors = false; + + // Get style + const style = element.style; + + switch (style.color.toLowerCase()) { + case 'rgb(0, 128, 0)': + case '#008000': + case 'green': + break; + + default: + console.log('Invalid color:', style.color); + errors = true; + } + + toggleTest(key, !errors); + } else { + toggleTest(key, 'failed'); + } + }} + /> +
+
+ ); +} diff --git a/packages/react/src/iconify.ts b/packages/react/src/iconify.ts index b705d7b..aaf42c6 100644 --- a/packages/react/src/iconify.ts +++ b/packages/react/src/iconify.ts @@ -74,6 +74,7 @@ import type { // Render SVG import { render } from './render'; +import { merge } from '@iconify/core/lib/misc/merge'; /** * Export required types @@ -292,37 +293,154 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') { /** * Component */ -function component( - props: IconProps, - inline: boolean, - ref?: IconRef -): JSX.Element { - const icon = props.icon; +interface InternalIconProps extends IconProps { + _ref?: IconRef; + _inline: boolean; +} - // Check if icon is an object - if (typeof icon === 'object' && typeof icon.body === 'string') { - return render(fullIcon(icon), props, inline, ref); +type IconComponentData = Required | null; + +interface IconComponentState { + data: IconComponentData; +} + +interface ComponentAbortData { + name: string; + abort: IconifyIconLoaderAbort; +} + +class IconComponent extends React.Component< + InternalIconProps, + IconComponentState +> { + protected _icon: string; + protected _loading: ComponentAbortData | null; + + constructor(props: InternalIconProps) { + super(props); + this.state = { + // Render placeholder before component is mounted + data: null, + }; } - // Check if icon is a string - if (typeof icon === 'string') { - const iconName = stringToIcon(icon, true, true); - if (iconName) { - // Valid icon name - const iconData = getIconData(iconName); - if (iconData) { - // Icon is available - return render(iconData, props, inline, ref); - } - - // TODO: icon is missing + /** + * Abort loading icon + */ + _abortLoading() { + if (this._loading) { + this._loading.abort(); + this._loading = null; } } - // Error - return props.children - ? (props.children as JSX.Element) - : React.createElement('span', {}); + /** + * Update state + */ + _setData(data: IconComponentData) { + if (this.state.data !== data) { + this.setState({ + data, + }); + } + } + + /** + * Check if icon should be loaded + */ + _checkIcon(changed: boolean) { + const state = this.state; + const icon = this.props.icon; + + // Icon is an object + if (typeof icon === 'object' && typeof icon.body === 'string') { + // Stop loading + this._icon = ''; + this._abortLoading(); + + if (changed || state.data === null) { + // Set data if it was changed + this._setData(fullIcon(icon)); + } + return; + } + + // Invalid icon? + if (typeof icon !== 'string') { + this._abortLoading(); + this._setData(null); + return; + } + + // Load icon + const data = getIconData(icon); + if (data === null) { + // Icon needs to be loaded + if (!this._loading || this._loading.name !== icon) { + // New icon to load + this._abortLoading(); + this._icon = ''; + this._setData(null); + this._loading = { + name: icon, + abort: API.loadIcons( + [icon], + this._checkIcon.bind(this, false) + ), + }; + } + return; + } + + // Icon data is available + if (this._icon !== icon || state.data === null) { + // New icon or icon has been loaded + this._abortLoading(); + this._icon = icon; + this._setData(data); + } + } + + /** + * Component mounted + */ + componentDidMount() { + this._checkIcon(false); + } + + /** + * Component updated + */ + componentDidUpdate(oldProps) { + if (oldProps.icon !== this.props.icon) { + this._checkIcon(true); + } + } + + /** + * Abort loading + */ + componentWillUnmount() { + this._abortLoading(); + } + + /** + * Render + */ + render() { + const props = this.props; + const data = this.state.data; + + if (data === null) { + // Render placeholder + return props.children + ? (props.children as JSX.Element) + : React.createElement('span', {}); + } + + // Render icon + return render(data, props, props._inline, props._ref); + } } /** @@ -336,7 +454,13 @@ export type Component = (props: IconProps) => JSX.Element; * @param props - Component properties */ export const Icon: Component = React.forwardRef( - (props: IconProps, ref?: IconRef) => component(props, false, ref) + (props: IconProps, ref?: IconRef) => { + const newProps = merge(props as Partial, { + _ref: ref, + _inline: false, + }) as InternalIconProps; + return React.createElement(IconComponent, newProps); + } ); /** @@ -345,5 +469,11 @@ export const Icon: Component = React.forwardRef( * @param props - Component properties */ export const InlineIcon: Component = React.forwardRef( - (props: IconProps, ref?: IconRef) => component(props, true, ref) + (props: IconProps, ref?: IconRef) => { + const newProps = merge(props as Partial, { + _ref: ref, + _inline: true, + }) as InternalIconProps; + return React.createElement(IconComponent, newProps); + } ); diff --git a/packages/react/src/render.ts b/packages/react/src/render.ts index 1c01c36..637c9e8 100644 --- a/packages/react/src/render.ts +++ b/packages/react/src/render.ts @@ -75,6 +75,9 @@ export const render = ( // Properties to ignore case 'icon': case 'style': + case 'children': + case '_ref': + case '_inline': break; // Flip as string: 'horizontal,vertical' diff --git a/packages/react/tests/api/20-rendering-from-api.js b/packages/react/tests/api/20-rendering-from-api.js deleted file mode 100644 index 333034c..0000000 --- a/packages/react/tests/api/20-rendering-from-api.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { Icon, loadIcons, iconExists } from '../../lib/iconify'; -import { mockAPIData } from '@iconify/core/lib/api/modules/mock'; -import { provider, nextPrefix } from './load'; - -const iconData = { - body: - '', - width: 24, - height: 24, -}; - -describe('Rendering icon', () => { - test('rendering icon after loading it', (done) => { - const prefix = nextPrefix(); - const name = 'mock-test'; - const iconName = `@${provider}:${prefix}:${name}`; - mockAPIData({ - provider, - prefix, - response: { - prefix, - icons: { - [name]: iconData, - }, - }, - }); - - // Check if icon has been loaded - expect(iconExists(iconName)).toEqual(false); - - // Load icon - loadIcons([iconName], (loaded, missing, pending) => { - // Make sure icon has been loaded - expect(loaded).toMatchObject([ - { - provider, - prefix, - name, - }, - ]); - expect(missing).toMatchObject([]); - expect(pending).toMatchObject([]); - expect(iconExists(iconName)).toEqual(true); - - // Render component - const component = renderer.create(); - const tree = component.toJSON(); - - expect(tree).toMatchObject({ - type: 'svg', - props: { - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlnsXlink': 'http://www.w3.org/1999/xlink', - 'aria-hidden': true, - 'role': 'img', - 'style': {}, - 'dangerouslySetInnerHTML': { - __html: iconData.body, - }, - 'width': '1em', - 'height': '1em', - 'preserveAspectRatio': 'xMidYMid meet', - 'viewBox': '0 0 ' + iconData.width + ' ' + iconData.height, - }, - children: null, - }); - - done(); - }); - }); -}); diff --git a/packages/react/tests/api/20-rendering-from-api.test.js b/packages/react/tests/api/20-rendering-from-api.test.js new file mode 100644 index 0000000..0f66f69 --- /dev/null +++ b/packages/react/tests/api/20-rendering-from-api.test.js @@ -0,0 +1,197 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { Icon, loadIcons, iconExists } from '../../lib/iconify'; +import { mockAPIData } from '@iconify/core/lib/api/modules/mock'; +import { provider, nextPrefix } from './load'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Rendering icon', () => { + test('rendering icon after loading it', (done) => { + const prefix = nextPrefix(); + const name = 'render-test'; + const iconName = `@${provider}:${prefix}:${name}`; + mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + [name]: iconData, + }, + }, + }); + + // Check if icon has been loaded + expect(iconExists(iconName)).toEqual(false); + + // Load icon + loadIcons([iconName], (loaded, missing, pending) => { + // Make sure icon has been loaded + expect(loaded).toMatchObject([ + { + provider, + prefix, + name, + }, + ]); + expect(missing).toMatchObject([]); + expect(pending).toMatchObject([]); + expect(iconExists(iconName)).toEqual(true); + + // Render component + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'svg', + props: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', + 'style': {}, + 'dangerouslySetInnerHTML': { + __html: iconData.body, + }, + 'width': '1em', + 'height': '1em', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox': '0 0 ' + iconData.width + ' ' + iconData.height, + }, + children: null, + }); + + done(); + }); + }); + + test('rendering icon before loading it', (done) => { + const prefix = nextPrefix(); + const name = 'mock-test'; + const iconName = `@${provider}:${prefix}:${name}`; + mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + [name]: iconData, + }, + }, + delay: (next) => { + // Icon should not have loaded yet + expect(iconExists(iconName)).toEqual(false); + + // Send icon data + next(); + + // Test it again + expect(iconExists(iconName)).toEqual(true); + + // Check if state was changed + // Wrapped in double setTimeout() because re-render takes 2 ticks + setTimeout(() => { + setTimeout(() => { + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'svg', + props: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', + 'style': {}, + 'dangerouslySetInnerHTML': { + __html: iconData.body, + }, + 'width': '1em', + 'height': '1em', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox': + '0 0 ' + + iconData.width + + ' ' + + iconData.height, + }, + children: null, + }); + + done(); + }, 0); + }, 0); + }, + }); + + // Check if icon has been loaded + expect(iconExists(iconName)).toEqual(false); + + // Render component + const component = renderer.create(); + const tree = component.toJSON(); + + // Should render placeholder + expect(tree).toMatchObject({ + type: 'span', + props: {}, + children: null, + }); + }); + + test('missing icon', (done) => { + const prefix = nextPrefix(); + const name = 'missing-icon'; + const iconName = `@${provider}:${prefix}:${name}`; + mockAPIData({ + provider, + prefix, + response: 404, + delay: (next) => { + // Icon should not have loaded yet + expect(iconExists(iconName)).toEqual(false); + + // Send icon data + next(); + + // Test it again + expect(iconExists(iconName)).toEqual(false); + + // Check if state was changed + // Wrapped in double setTimeout() because re-render takes 2 ticks + setTimeout(() => { + setTimeout(() => { + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'span', + props: {}, + children: null, + }); + + done(); + }, 0); + }, 0); + }, + }); + + // Check if icon has been loaded + expect(iconExists(iconName)).toEqual(false); + + // Render component + const component = renderer.create(); + const tree = component.toJSON(); + + // Should render placeholder + expect(tree).toMatchObject({ + type: 'span', + props: {}, + children: null, + }); + }); +}); diff --git a/packages/react/tests/api/30-changing-props.test.js b/packages/react/tests/api/30-changing-props.test.js new file mode 100644 index 0000000..734a509 --- /dev/null +++ b/packages/react/tests/api/30-changing-props.test.js @@ -0,0 +1,256 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { Icon, iconExists } from '../../lib/iconify'; +import { mockAPIData } from '@iconify/core/lib/api/modules/mock'; +import { provider, nextPrefix } from './load'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +const iconData2 = { + body: + '', + width: 32, + height: 32, +}; + +describe('Rendering icon', () => { + test('changing icon property', (done) => { + const prefix = nextPrefix(); + const name = 'changing-prop'; + const name2 = 'changing-prop2'; + const iconName = `@${provider}:${prefix}:${name}`; + const iconName2 = `@${provider}:${prefix}:${name2}`; + + mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + [name]: iconData, + }, + }, + delay: (next) => { + // Icon should not have loaded yet + expect(iconExists(iconName)).toEqual(false); + + // Send icon data + next(); + + // Test it again + expect(iconExists(iconName)).toEqual(true); + + // Check if state was changed + // Wrapped in double setTimeout() because re-render takes 2 ticks + setTimeout(() => { + setTimeout(() => { + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'svg', + props: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', + 'style': {}, + 'dangerouslySetInnerHTML': { + __html: iconData.body, + }, + 'width': '1em', + 'height': '1em', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox': + '0 0 ' + + iconData.width + + ' ' + + iconData.height, + }, + children: null, + }); + + component.update(); + }, 0); + }, 0); + }, + }); + + mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + [name2]: iconData2, + }, + }, + delay: (next) => { + // Icon should not have loaded yet + expect(iconExists(iconName2)).toEqual(false); + + // Send icon data + next(); + + // Test it again + expect(iconExists(iconName2)).toEqual(true); + + // Check if state was changed + // Wrapped in double setTimeout() because re-render takes 2 ticks + setTimeout(() => { + setTimeout(() => { + const tree = component.toJSON(); + + expect(tree).toMatchObject({ + type: 'svg', + props: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', + 'style': {}, + 'dangerouslySetInnerHTML': { + __html: iconData2.body, + }, + 'width': '1em', + 'height': '1em', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox': + '0 0 ' + + iconData2.width + + ' ' + + iconData2.height, + }, + children: null, + }); + + done(); + }, 0); + }, 0); + }, + }); + + // Check if icon has been loaded + expect(iconExists(iconName)).toEqual(false); + + // Render component + const component = renderer.create(); + const tree = component.toJSON(); + + // Should render placeholder + expect(tree).toMatchObject({ + type: 'span', + props: {}, + children: null, + }); + }); + + test('changing multiple properties', (done) => { + const prefix = nextPrefix(); + const name = 'multiple-props'; + const iconName = `@${provider}:${prefix}:${name}`; + + mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + [name]: iconData, + }, + }, + delay: (next) => { + // Icon should not have loaded yet + expect(iconExists(iconName)).toEqual(false); + + // Send icon data + next(); + + // Test it again + expect(iconExists(iconName)).toEqual(true); + + // Check if state was changed + // Wrapped in double setTimeout() because re-render takes 2 ticks + setTimeout(() => { + setTimeout(() => { + let tree = component.toJSON(); + expect(tree).toMatchObject({ + type: 'svg', + props: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', + 'style': {}, + 'dangerouslySetInnerHTML': { + __html: iconData.body, + }, + 'width': '1em', + 'height': '1em', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox': + '0 0 ' + + iconData.width + + ' ' + + iconData.height, + }, + children: null, + }); + + // Add horizontal flip and style + component.update( + + ); + + tree = component.toJSON(); + expect(tree).toMatchObject({ + type: 'svg', + props: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlnsXlink': 'http://www.w3.org/1999/xlink', + 'aria-hidden': true, + 'role': 'img', + 'style': { + color: 'red', + }, + 'dangerouslySetInnerHTML': { + __html: `${iconData.body}`, + }, + 'width': '1em', + 'height': '1em', + 'preserveAspectRatio': 'xMidYMid meet', + 'viewBox': + '0 0 ' + + iconData.width + + ' ' + + iconData.height, + }, + children: null, + }); + done(); + }, 0); + }, 0); + }, + }); + + // Check if icon has been loaded + expect(iconExists(iconName)).toEqual(false); + + // Render component with placeholder text + const component = renderer.create( + loading... + ); + const tree = component.toJSON(); + + // Should render placeholder + expect(tree).toEqual('loading...'); + }); +}); diff --git a/packages/react/tests/api/30-ref.test.js b/packages/react/tests/api/30-ref.test.js new file mode 100644 index 0000000..6ea39a9 --- /dev/null +++ b/packages/react/tests/api/30-ref.test.js @@ -0,0 +1,188 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { Icon, InlineIcon, loadIcons, iconExists } from '../../lib/iconify'; +import { mockAPIData } from '@iconify/core/lib/api/modules/mock'; +import { provider, nextPrefix } from './load'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Testing references', () => { + test('reference for preloaded icon', (done) => { + const prefix = nextPrefix(); + const name = 'render-test'; + const iconName = `@${provider}:${prefix}:${name}`; + mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + [name]: iconData, + }, + }, + }); + + // Check if icon has been loaded + expect(iconExists(iconName)).toEqual(false); + + // Load icon + loadIcons([iconName], (loaded, missing, pending) => { + let gotRef = false; + let gotInlineRef = false; + + // Make sure icon has been loaded + expect(loaded).toMatchObject([ + { + provider, + prefix, + name, + }, + ]); + expect(missing).toMatchObject([]); + expect(pending).toMatchObject([]); + expect(iconExists(iconName)).toEqual(true); + + // Render components + renderer.create( + { + gotRef = true; + }} + /> + ); + + renderer.create( + { + gotInlineRef = true; + }} + /> + ); + + // References should be called immediately in test + expect(gotRef).toEqual(true); + expect(gotInlineRef).toEqual(true); + + done(); + }); + }); + + test('reference to pending icon', (done) => { + const prefix = nextPrefix(); + const name = 'mock-test'; + const iconName = `@${provider}:${prefix}:${name}`; + let gotRef = false; + + mockAPIData({ + provider, + prefix, + response: { + prefix, + icons: { + [name]: iconData, + }, + }, + delay: (next) => { + // Icon should not have loaded yet + expect(iconExists(iconName)).toEqual(false); + + // Reference should not have been called yet + expect(gotRef).toEqual(false); + + // Send icon data + next(); + + // Test it again + expect(iconExists(iconName)).toEqual(true); + expect(gotRef).toEqual(false); + + // Check if state was changed + // Wrapped in double setTimeout() because re-render takes 2 ticks + setTimeout(() => { + setTimeout(() => { + expect(gotRef).toEqual(true); + + done(); + }, 0); + }, 0); + }, + }); + + // Check if icon has been loaded + expect(iconExists(iconName)).toEqual(false); + + // Render component + renderer.create( + { + gotRef = true; + }} + /> + ); + + // Reference should not have been called yet + expect(gotRef).toEqual(false); + }); + + test('missing icon', (done) => { + const prefix = nextPrefix(); + const name = 'missing-icon'; + const iconName = `@${provider}:${prefix}:${name}`; + let gotRef = false; + + mockAPIData({ + provider, + prefix, + response: 404, + delay: (next) => { + // Icon should not have loaded yet + expect(iconExists(iconName)).toEqual(false); + + // Reference should not have been called + expect(gotRef).toEqual(false); + + // Send icon data + next(); + + // Test it again + expect(iconExists(iconName)).toEqual(false); + expect(gotRef).toEqual(false); + + // Check if state was changed + // Wrapped in double setTimeout() because re-render takes 2 ticks + setTimeout(() => { + setTimeout(() => { + // Reference should not have been called + expect(gotRef).toEqual(false); + + done(); + }, 0); + }, 0); + }, + }); + + // Check if icon has been loaded + expect(iconExists(iconName)).toEqual(false); + + // Render component + const component = renderer.create( + { + gotRef = true; + }} + > + ); + + // Reference should not have been called + expect(gotRef).toEqual(false); + }); +}); diff --git a/packages/react/tests/iconify/20-ref.test.js b/packages/react/tests/iconify/20-ref.test.js new file mode 100644 index 0000000..01b2931 --- /dev/null +++ b/packages/react/tests/iconify/20-ref.test.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { Icon, InlineIcon } from '../../lib/iconify'; +import renderer from 'react-test-renderer'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Testing references', () => { + test('basic icon reference', () => { + let gotRef = false; + const component = renderer.create( + { + gotRef = true; + }} + /> + ); + + // Ref should have been called by now + expect(gotRef).toEqual(true); + }); + + test('inline icon reference', () => { + let gotRef = false; + const component = renderer.create( + { + gotRef = true; + }} + /> + ); + + // Ref should have been called by now + expect(gotRef).toEqual(true); + }); + + test('placeholder reference', () => { + let gotRef = false; + const component = renderer.create( + { + gotRef = true; + }} + /> + ); + + // Ref should not have been called + expect(gotRef).toEqual(false); + }); +}); diff --git a/packages/react/tests/offline/20-ref.test.js b/packages/react/tests/offline/20-ref.test.js new file mode 100644 index 0000000..3e3d6e2 --- /dev/null +++ b/packages/react/tests/offline/20-ref.test.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { Icon, InlineIcon } from '../../lib/offline'; +import renderer from 'react-test-renderer'; + +const iconData = { + body: + '', + width: 24, + height: 24, +}; + +describe('Testing references', () => { + test('basic icon reference', () => { + let gotRef = false; + const component = renderer.create( + { + gotRef = true; + }} + /> + ); + + // Ref should have been called by now + expect(gotRef).toEqual(true); + }); + + test('inline icon reference', () => { + let gotRef = false; + const component = renderer.create( + { + gotRef = true; + }} + /> + ); + + // Ref should have been called by now + expect(gotRef).toEqual(true); + }); + + test('placeholder reference', () => { + let gotRef = false; + const component = renderer.create( + { + gotRef = true; + }} + /> + ); + + // Ref should not have been called + expect(gotRef).toEqual(false); + }); +});