Skip to content
This repository was archived by the owner on Sep 26, 2023. It is now read-only.

Commit 2eb10c1

Browse files
committed
More tests
1 parent 753101a commit 2eb10c1

12 files changed

+201
-31
lines changed

jest-setup.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
import '@testing-library/jest-dom';
2+
3+
global.afterEach(() => {
4+
document.body.replaceChildren();
5+
});

src/db.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GroupMessage, Emoji, GroupKey } from 'emojibase';
1+
import { GroupMessage, Emoji } from 'emojibase';
22
import { EmojiRecord, Category, CategoryKey } from './types';
33

44
import { caseInsensitiveIncludes } from './util';
File renamed without changes.

src/options.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ const defaultOptions: Partial<PickerOptions> = {
3434
custom: []
3535
};
3636

37-
export function getOptions(options: Partial<PickerOptions>): PickerOptions {
37+
export function getOptions(options: Partial<PickerOptions> = {}): PickerOptions {
3838
return { ...defaultOptions, ...options } as PickerOptions;
3939
}

src/viewFactory.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ type DependencyMapping = {
1616
pickerId: string;
1717
};
1818

19-
type ViewConstructor<T extends View> = new (...args: any[]) => T;
20-
type ViewConstructorParameters<T extends View> = ConstructorParameters<ViewConstructor<T>>;
19+
export type ViewConstructor<T extends View> = new (...args: any[]) => T;
20+
export type ViewConstructorParameters<T extends View> = ConstructorParameters<ViewConstructor<T>>;
2121

2222
export class ViewFactory {
2323
private events: Events<AppEvent>;

src/views/CategoryTab.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const focusEventOptions = { scroll: 'animate', focus: 'button', performFocus: tr
1515
export class CategoryTab extends View {
1616
private category: Category;
1717
private icon: string;
18+
19+
isActive = false;
1820

1921
constructor({ category, icon }: CategoryTabOptions) {
2022
super({ template, classes });
@@ -36,21 +38,27 @@ export class CategoryTab extends View {
3638
super.initialize();
3739
}
3840

39-
async render(): Promise<HTMLElement> {
40-
return super.render({
41+
renderSync(): HTMLElement {
42+
super.renderSync({
4143
category: this.category,
4244
icon: this.icon
4345
});
46+
47+
this.ui.button.ariaSelected = 'false';
48+
49+
return this.el;
4450
}
4551

4652
setActive(isActive: boolean, changeFocus = true) {
53+
this.isActive = isActive;
4754
this.ui.button.classList.toggle(classes.categoryButtonActive, isActive);
4855
if (changeFocus) {
4956
this.setFocused(isActive);
5057
}
5158
}
5259

5360
private setFocused(isFocused: boolean) {
61+
this.ui.button.ariaSelected = isFocused.toString();
5462
if (isFocused) {
5563
this.ui.button.tabIndex = 0;
5664
this.ui.button.focus();
@@ -59,7 +67,9 @@ export class CategoryTab extends View {
5967
}
6068
}
6169

62-
selectCategory() {
63-
this.events.emit('category:select', this.category.key, focusEventOptions);
70+
private selectCategory() {
71+
if (!this.isActive) {
72+
this.events.emit('category:select', this.category.key, focusEventOptions);
73+
}
6474
}
6575
}

src/views/CategoryTabs.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class CategoryTabs extends View {
5050
this.viewFactory.create(CategoryTab, { category, icon: categoryIcons[category.key] }));
5151

5252
await super.render({
53-
tabs: await Promise.all(this.tabViews.map(view => view.render()))
53+
tabs: this.tabViews.map(view => view.renderSync())
5454
});
5555

5656
return this.el;
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import userEvent from '@testing-library/user-event';
2+
import { fireEvent } from '@testing-library/dom';
3+
import { testViewSync } from '../../../testHelpers/testView';
4+
5+
import { Events } from '../../events';
6+
import { AppEvent } from '../../AppEvents';
7+
8+
import { CategoryTab } from '../CategoryTab';
9+
import { Category } from '../../types';
10+
11+
describe('CategoryTab', () => {
12+
const category: Category = {
13+
key: 'smileys-emotion',
14+
message: 'Smileys & Emotion',
15+
order: 0
16+
};
17+
18+
const events = new Events<AppEvent>();
19+
const emitSpy = jest.spyOn(events, 'emit');
20+
21+
const icon = 'fa-face-smile';
22+
23+
afterEach(() => {
24+
emitSpy.mockReset();
25+
});
26+
27+
function renderTab() {
28+
return testViewSync(CategoryTab, [{ category, icon }], { events });
29+
}
30+
31+
test('renders a category tab', () => {
32+
const tab = renderTab();
33+
34+
const button = tab.ui.button;
35+
expect(button).toHaveAttribute('data-category', 'smileys-emotion');
36+
expect(button).toHaveAttribute('title', 'Smileys & Emotion');
37+
expect(button).toHaveAttribute('tabindex', '-1');
38+
});
39+
40+
test('toggles the active status', () => {
41+
const tab = renderTab();
42+
const button = tab.ui.button;
43+
expect(button.ariaSelected).toBe('false');
44+
45+
// By default it should make the tab focusable and set aria-selected to true
46+
tab.setActive(true);
47+
expect(button.ariaSelected).toBe('true');
48+
expect(button.tabIndex).toBe(0);
49+
50+
// Deactivation should reset tabindex and aria-selected
51+
tab.setActive(false);
52+
expect(button.ariaSelected).toBe('false');
53+
expect(button.tabIndex).toBe(-1);
54+
55+
// Setting active without focus (like when scrolling) should not change tabindex or aria-selected
56+
tab.setActive(true, false);
57+
expect(button.ariaSelected).toBe('false');
58+
});
59+
60+
test('emits a category:select event when clicked', () => {
61+
const tab = renderTab();
62+
const button = tab.ui.button;
63+
64+
userEvent.click(button);
65+
expect(events.emit).toHaveBeenCalledWith('category:select', 'smileys-emotion', expect.anything());
66+
});
67+
68+
test('emits a category:select event when focused', () => {
69+
const tab = renderTab();
70+
const button = tab.ui.button;
71+
72+
fireEvent.focus(button);
73+
expect(events.emit).toHaveBeenCalledWith('category:select', 'smileys-emotion', expect.anything());
74+
});
75+
});

src/views/__tests__/Emoji.test.ts

+51-14
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,62 @@
1-
import { screen } from '@testing-library/dom';
1+
import { fireEvent } from '@testing-library/dom';
22

33
import { Emoji } from '../Emoji';
4-
import { renderSyncToBody } from '../view';
4+
import { testViewSync } from '../../../testHelpers/testView';
55

6-
import NativeRenderer from '../../renderers/native';
6+
import { Events } from '../../events';
7+
import { AppEvent } from '../../AppEvents';
8+
import { CategoryKey } from '../../types';
79

810
describe('Emoji', () => {
11+
const emojiData = {
12+
emoji: '😎',
13+
label: 'smile'
14+
};
15+
16+
const events = new Events<AppEvent>();
17+
const emitSpy = jest.spyOn(events, 'emit');
18+
19+
function renderEmoji(category?: CategoryKey) {
20+
return testViewSync(Emoji, [{ emoji: emojiData, category }], {
21+
events
22+
});
23+
}
24+
25+
afterEach(() => {
26+
emitSpy.mockReset();
27+
});
28+
929
test('renders an emoji', async () => {
10-
const emojiData = {
11-
emoji: '😎',
12-
label: 'smile'
13-
};
30+
const { el } = renderEmoji();
31+
expect(el).toHaveTextContent('😎');
32+
expect(el).toHaveAttribute('title', 'smile');
33+
expect(el).toHaveAttribute('data-emoji', '😎');
34+
});
35+
36+
test('emits focus:change event when the emoji receives focus and there is a category', () => {
37+
const { el } = renderEmoji('smileys-emotion');
38+
fireEvent.focus(el);
39+
expect(events.emit).toHaveBeenCalledWith('focus:change', 'smileys-emotion');
40+
});
41+
42+
test('does not emit focus:change event if there is no category set', () => {
43+
const { el } = renderEmoji();
44+
fireEvent.focus(el);
45+
expect(events.emit).not.toHaveBeenCalled();
46+
});
1447

15-
const emoji = new Emoji({ emoji: emojiData });
16-
emoji.setRenderer(new NativeRenderer());
48+
test('activates/deactivates focus', () => {
49+
const emoji = renderEmoji();
50+
expect(emoji.el).toHaveAttribute('tabindex', '-1');
1751

18-
renderSyncToBody(emoji);
52+
emoji.activateFocus();
53+
expect(document.activeElement).not.toBe(emoji.el);
54+
expect(emoji.el).toHaveAttribute('tabindex', '0');
1955

20-
const button = screen.getByRole('button');
21-
expect(button).toHaveTextContent('😎');
22-
expect(button).toHaveAttribute('title', 'smile');
23-
expect(button).toHaveAttribute('data-emoji', '😎');
56+
emoji.deactivateFocus();
57+
expect(emoji.el).toHaveAttribute('tabindex', '-1');
58+
59+
emoji.activateFocus(true);
60+
expect(document.activeElement).toBe(emoji.el);
2461
});
2562
});

src/views/view.ts

-8
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,3 @@ export abstract class View {
216216
return `.${className}`;
217217
}
218218
}
219-
220-
export function renderSyncToBody(view: View) {
221-
document.body.appendChild(view.renderSync());
222-
}
223-
224-
export async function renderToBody(view: View) {
225-
document.body.appendChild(await view.render());
226-
}

testHelpers/mockDatabase.ts

Whitespace-only changes.

testHelpers/testView.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ViewConstructor, ViewConstructorParameters, ViewFactory } from '../src/viewFactory';
2+
import { View } from '../src/views/view';
3+
4+
import { Events } from '../src/events';
5+
import { AppEvent } from '../src/AppEvents';
6+
import { Bundle } from '../src/i18n';
7+
import NativeRenderer from '../src/renderers/native';
8+
import { Database } from '../src/db';
9+
10+
import { getOptions } from '../src/options';
11+
12+
jest.mock('../src/db');
13+
14+
const defaultDependencies = {
15+
events: new Events<AppEvent>(),
16+
i18n: new Bundle(),
17+
renderer: new NativeRenderer(),
18+
options: getOptions({ i18n: {} }),
19+
emojiData: Promise.resolve(new Database()),
20+
customEmojis: [],
21+
pickerId: 'test-picker'
22+
};
23+
24+
function createTestView<T extends View>(
25+
constructor: ViewConstructor<T>,
26+
args: ViewConstructorParameters<T>,
27+
dependencies = {}
28+
) {
29+
const factory = new ViewFactory({ ...defaultDependencies, ...dependencies });
30+
const view = factory.create(constructor, ...args);
31+
return view;
32+
}
33+
34+
export async function testView<T extends View>(
35+
constructor: ViewConstructor<T>,
36+
args: ViewConstructorParameters<T>,
37+
dependencies = {}
38+
) {
39+
const view = createTestView(constructor, args, dependencies);
40+
document.body.appendChild(await view.render());
41+
return view;
42+
}
43+
44+
export function testViewSync<T extends View>(
45+
constructor: ViewConstructor<T>,
46+
args: ViewConstructorParameters<T>,
47+
dependencies = {}
48+
) {
49+
const view = createTestView(constructor, args, dependencies);
50+
document.body.appendChild(view.renderSync());
51+
return view;
52+
}

0 commit comments

Comments
 (0)