diff --git a/src/components/icon-button/icon-button.test.ts b/src/components/icon-button/icon-button.test.ts new file mode 100644 index 000000000..351e5c961 --- /dev/null +++ b/src/components/icon-button/icon-button.test.ts @@ -0,0 +1,121 @@ +import { expect, fixture, html, waitUntil } from '@open-wc/testing'; +import type SlIconButton from './icon-button'; + +describe('', () => { + describe('defaults ', () => { + it('default properties', async () => { + const el = await fixture(html` `); + + expect(el.name).to.be.undefined; + expect(el.library).to.be.undefined; + expect(el.src).to.be.undefined; + expect(el.href).to.be.undefined; + expect(el.target).to.be.undefined; + expect(el.download).to.be.undefined; + expect(el.label).to.equal(''); + expect(el.disabled).to.equal(false); + }); + + it('renders as a button by default', async () => { + const el = await fixture(html` `); + + expect(el.shadowRoot?.querySelector('button')).to.exist; + expect(el.shadowRoot?.querySelector('a')).not.to.exist; + }); + }); + + describe('when icon attributes are present', () => { + it('renders an sl-icon from a library', async () => { + const el = await fixture( + html` ` + ); + expect(el.shadowRoot?.querySelector('sl-icon')).to.exist; + }); + + it('renders an sl-icon from a src', async () => { + const fakeId = 'test-src'; + const el = await fixture(html` `); + + el.src = `data:image/svg+xml,${encodeURIComponent(``)}`; + + const internalSlIcon = el.shadowRoot?.querySelector('sl-icon'); + + await waitUntil(() => internalSlIcon?.shadowRoot?.querySelector('svg'), 'SVG not rendered'); + + expect(internalSlIcon).to.exist; + expect(internalSlIcon?.shadowRoot?.querySelector('svg')).to.exist; + expect(internalSlIcon?.shadowRoot?.querySelector('svg')?.getAttribute('id')).to.equal(fakeId); + }); + }); + + describe('when href is present', () => { + it('renders as an anchor', async () => { + const el = await fixture(html` `); + + expect(el.shadowRoot?.querySelector('a')).to.exist; + expect(el.shadowRoot?.querySelector('button')).not.to.exist; + }); + + it(`the anchor rel is not present`, async () => { + const el = await fixture(html` `); + expect(el.shadowRoot?.querySelector(`a[rel]`)).not.to.exist; + }); + + describe('and target is present', () => { + ['_blank', '_parent', '_self', '_top'].forEach((target: string) => { + it(`the anchor target is the provided target: ${target}`, async () => { + const el = await fixture( + html` ` + ); + expect(el.shadowRoot?.querySelector(`a[target="${target}"]`)).to.exist; + }); + + it(`the anchor rel is set to 'noreferrer noopener'`, async () => { + const el = await fixture( + html` ` + ); + expect(el.shadowRoot?.querySelector(`a[rel="noreferrer noopener"]`)).to.exist; + }); + }); + }); + + describe('and download is present', () => { + it(`the anchor downlown attribute is the provided download`, async () => { + const fakeDownload = 'some/path'; + const el = await fixture( + html` ` + ); + + expect(el.shadowRoot?.querySelector(`a[download="${fakeDownload}"]`)).to.exist; + }); + }); + }); + + describe('when label is present', () => { + it('the internal aria-label attribute is set to the provided label when rendering a button', async () => { + const fakeLabel = 'some label'; + const el = await fixture(html` `); + expect(el.shadowRoot?.querySelector(`button[aria-label="${fakeLabel}"]`)).to.exist; + }); + + it('the internal aria-label attribute is set to the provided label when rendering an anchor', async () => { + const fakeLabel = 'some label'; + const el = await fixture( + html` ` + ); + expect(el.shadowRoot?.querySelector(`a[aria-label="${fakeLabel}"]`)).to.exist; + }); + }); + + describe('when disabled is present', () => { + it('the internal button has a disabled attribute when rendering a button', async () => { + const el = await fixture(html` `); + expect(el.shadowRoot?.querySelector(`button[disabled]`)).to.exist; + }); + + it('the internal anchor has an aria-disabled attribute when rendering an anchor', async () => { + const el = await fixture(html` `); + expect(el.shadowRoot?.querySelector(`a[aria-disabled="true"]`)).to.exist; + }); + }); +}); diff --git a/src/components/icon/icon.test.ts b/src/components/icon/icon.test.ts new file mode 100644 index 000000000..91d92fbf5 --- /dev/null +++ b/src/components/icon/icon.test.ts @@ -0,0 +1,179 @@ +/* eslint-disable no-restricted-imports */ +import { elementUpdated, expect, fixture, html, oneEvent } from '@open-wc/testing'; +// import sinon from 'sinon'; +/* @ts-expect-error - Need to switch to path aliases when Web Test Runner's esbuild plugin allows it */ +import { registerIconLibrary } from '../../../dist/shoelace.js'; +import type SlIcon from './icon'; + +const testLibraryIcons = { + 'test-icon1': ` + + `, + 'test-icon2': ` + + `, + 'bad-icon': `
` +}; + +describe('', () => { + before(() => { + registerIconLibrary('test-library', { + resolver: (name: keyof typeof testLibraryIcons) => { + // only for testing a bad request + if (name === ('bad-request' as keyof typeof testLibraryIcons)) { + return `data:image/svg+xml`; + } + + if (name in testLibraryIcons) { + return `data:image/svg+xml,${encodeURIComponent(testLibraryIcons[name])}`; + } + return ''; + }, + mutator: (svg: SVGElement) => svg.setAttribute('fill', 'currentColor') + }); + }); + + describe('defaults ', () => { + it('default properties', async () => { + const el = await fixture(html` `); + + expect(el.name).to.be.undefined; + expect(el.src).to.be.undefined; + expect(el.label).to.equal(''); + expect(el.library).to.equal('default'); + }); + + it('renders pre-loaded system icons and emits sl-load event', async () => { + const el = await fixture(html` `); + const listener = oneEvent(el, 'sl-load'); + + el.name = 'check-lg'; + const ev = await listener; + await elementUpdated(el); + + expect(el.shadowRoot?.querySelector('svg')).to.exist; + expect(ev).to.exist; + }); + + it('the icon is accessible', async () => { + const el = await fixture(html` `); + await expect(el).to.be.accessible(); + }); + + it('the icon has the correct default aria attributes', async () => { + const el = await fixture(html` `); + const rootDiv = el.shadowRoot?.querySelector('div.icon'); + + expect(rootDiv?.getAttribute('role')).to.be.null; + expect(rootDiv?.getAttribute('aria-label')).to.be.null; + expect(rootDiv?.getAttribute('aria-hidden')).to.equal('true'); + }); + }); + + describe('when a label is provided', () => { + it('the icon has the correct default aria attributes', async () => { + const fakeLabel = 'a label'; + const el = await fixture(html` `); + const rootDiv = el.shadowRoot?.querySelector('div.icon'); + + expect(rootDiv?.getAttribute('role')).to.equal('img'); + expect(rootDiv?.getAttribute('aria-label')).to.equal(fakeLabel); + expect(rootDiv?.getAttribute('aria-hidden')).to.be.null; + }); + }); + + describe('whena valid src is provided', () => { + it('the svg is rendered', async () => { + const fakeId = 'test-src'; + const el = await fixture(html` `); + + const listener = oneEvent(el, 'sl-load'); + el.src = `data:image/svg+xml,${encodeURIComponent(``)}`; + + await listener; + await elementUpdated(el); + + expect(el.shadowRoot?.querySelector('svg')).to.exist; + expect(el.shadowRoot?.querySelector('svg')?.getAttribute('id')).to.equal(fakeId); + }); + }); + + describe('new library', () => { + it('renders icons from the new library and emits sl-load event', async () => { + const el = await fixture(html` `); + const listener = oneEvent(el, 'sl-load'); + + el.name = 'test-icon1'; + const ev = await listener; + await elementUpdated(el); + + expect(el.shadowRoot?.querySelector('svg')).to.exist; + expect(ev.isTrusted).to.exist; + }); + + it('runs mutators from new library', async () => { + const el = await fixture(html` `); + await elementUpdated(el); + + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('fill')).to.equal('currentColor'); + }); + }); + + describe('negative cases', () => { + // using new library so we can test for malformed icons when registered + it('svg not rendered with an icon that doesnt exist in the library', async () => { + const el = await fixture(html` `); + + expect(el.shadowRoot?.querySelector('svg')).to.be.null; + }); + + it('emits sl-error when the file cant be retrieved', async () => { + const el = await fixture(html` `); + const listener = oneEvent(el, 'sl-error'); + + el.name = 'bad-request'; + const ev = await listener; + await elementUpdated(el); + + expect(el.shadowRoot?.querySelector('svg')).to.be.null; + expect(ev).to.exist; + }); + + it('emits sl-error when there isnt an svg element in the registered icon', async () => { + const el = await fixture(html` `); + const listener = oneEvent(el, 'sl-error'); + + el.name = 'bad-icon'; + const ev = await listener; + await elementUpdated(el); + + expect(el.shadowRoot?.querySelector('svg')).to.be.null; + expect(ev).to.exist; + }); + }); + + // describe('cached icon request timing', () => { + // it('sl-load event doesnt fire until after firstUpdated, even when the icon is cached', async () => { + // // have to use an icon not used in tests before + // // div in fixture so we can append another icon + // const el = await fixture(html`
`); + + // const newIcon = document.createElement('sl-icon'); + // newIcon.library = 'system'; + // newIcon.name = 'chevron-down'; + + // let updateCount: number; + // newIcon.updateComplete.then(() => { + // updateCount = updateCount++; + // }); + + // newIcon.addEventListener('sl-load', () => { + // expect(updateCount).to.equal(1); + // }); + + // el.append(newIcon); + + // }); + // }); +}); diff --git a/src/components/icon/library.ts b/src/components/icon/library.ts index a0133d88d..2ac63c5ad 100644 --- a/src/components/icon/library.ts +++ b/src/components/icon/library.ts @@ -28,7 +28,7 @@ export function getIconLibrary(name?: string) { export function registerIconLibrary( name: string, options: { resolver: IconLibraryResolver; mutator?: IconLibraryMutator } -) { +): void { unregisterIconLibrary(name); registry.push({ name,