Skip to content

Commit

Permalink
✨ [Earwurm] Add keys event
Browse files Browse the repository at this point in the history
  • Loading branch information
beefchimi committed Dec 22, 2023
1 parent f2ba7bd commit b574cbf
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 4 deletions.
12 changes: 10 additions & 2 deletions src/Earwurm.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {EmittenCommon} from 'emitten';

import {getErrorMessage, unlockAudioContext} from './helpers';
import {clamp, msToSec, secToMs} from './utilities';
import {arrayShallowEquals, clamp, msToSec, secToMs} from './utilities';
import {tokens} from './tokens';

import type {
Expand Down Expand Up @@ -255,8 +255,16 @@ export class Earwurm extends EmittenCommon<ManagerEventMap> {
}

#setLibrary(library: Stack[]) {
const oldKeys = this._keys;
const newKeys = library.map(({id}) => id);
const identicalKeys = arrayShallowEquals(oldKeys, newKeys);

this.#library = library;
this._keys = this.#library.map(({id}) => id);
this._keys = newKeys;

if (!identicalKeys) {
this.emit('keys', newKeys, oldKeys);
}
}

#setState(value: ManagerState) {
Expand Down
73 changes: 73 additions & 0 deletions src/tests/Earwurm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('Earwurm component', () => {
{id: 'One', path: 'to/no/file.mp3'},
{id: 'Two', path: ''},
];
const mockInitialKeys: LibraryKeys = mockEntries.map(({id}) => id);

afterEach(() => {
mockManager.teardown();
Expand Down Expand Up @@ -250,6 +251,50 @@ describe('Earwurm component', () => {
expect(mockManager.keys).toHaveLength(4);
});

it('emits `keys` event with new and old `keys`', async () => {
const spyKeysChange: ManagerEventMap['keys'] = vi.fn((_value) => {});

mockManager.on('keys', spyKeysChange);
expect(spyKeysChange).not.toBeCalled();

mockManager.add(...mockEntries);
expect(spyKeysChange).toBeCalledWith(mockInitialKeys, []);
expect(spyKeysChange).toBeCalledTimes(1);

// Does not add/remove when both `id + path` are identical.
mockManager.add(mockEntries[0]);
expect(spyKeysChange).not.toBeCalledTimes(2);

const mockUniqueEntry: LibraryEntry = {
id: 'Unique',
path: 'does/not/overwrite/anything.wav',
};
const mockChangedEntries: LibraryEntry[] = [
mockUniqueEntry,
mockEntries[1],
];

mockManager.add(...mockChangedEntries);
expect(spyKeysChange).toBeCalledTimes(2);
expect(spyKeysChange).toBeCalledWith(
[...mockInitialKeys, mockUniqueEntry.id],
mockInitialKeys,
);

const keysSnapshot = mockManager.keys;

// Emits twice as an existing key is removed then re-added
// as a result of the `path` value changing.
mockManager.add({...mockUniqueEntry, path: 'changed'});
expect(spyKeysChange).toBeCalledTimes(4);

expect(spyKeysChange).toBeCalledWith(mockInitialKeys, keysSnapshot);
expect(spyKeysChange).toHaveBeenLastCalledWith(
keysSnapshot,
mockInitialKeys,
);
});

// TODO: Figure out how best to read `fadeMs` and `request` from Stack.
it.skip('passes `fadeMs` and `request` to Stack', async () => {
const mockConfig: ManagerConfig = {
Expand Down Expand Up @@ -291,6 +336,22 @@ describe('Earwurm component', () => {
expect(capturedKeys).toStrictEqual([]);
});

it('emits `keys` event with new and old `keys`', async () => {
const spyKeysChange: ManagerEventMap['keys'] = vi.fn((_value) => {});

mockManager.add(...mockEntries);
mockManager.on('keys', spyKeysChange);

mockManager.remove('Foo', 'Bar');
expect(spyKeysChange).not.toBeCalled();

mockManager.remove(mockEntries[1].id);
expect(spyKeysChange).toBeCalledWith(
[mockEntries[0].id, mockEntries[2].id],
mockInitialKeys,
);
});

it('tears down Stacks before removing from library', async () => {
const mockChangedEntries: LibraryEntry[] = [
{
Expand Down Expand Up @@ -394,6 +455,18 @@ describe('Earwurm component', () => {
expect(mockManager.keys).toStrictEqual([]);
});

it('emits `keys` event with empty array', async () => {
const spyKeysChange: ManagerEventMap['keys'] = vi.fn((_value) => {});

mockManager.add(...mockEntries);

mockManager.on('keys', spyKeysChange);
expect(spyKeysChange).not.toBeCalled();

mockManager.teardown();
expect(spyKeysChange).toBeCalledWith([], mockInitialKeys);
});

it('does not resume the AudioContext', async () => {
mockManager.unlock();
document.dispatchEvent(clickEvent);
Expand Down
8 changes: 6 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ export type ManagerState = AudioContextState | 'suspending' | 'interrupted';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ManagerEventMap = {
statechange: (state: ManagerState) => void;
error: (error: CombinedErrorMessage) => void;
// TODO: Rename this to `state`.
statechange: (current: ManagerState) => void;
error: (message: CombinedErrorMessage) => void;
volume: (level: number) => void;
mute: (muted: boolean) => void;
// This is not the same as a "library change". This event
// will not fire if an identical `entry > id` updates it's `path`.
keys: (newKeys: LibraryKeys, oldKeys: LibraryKeys) => void;
};

export interface ManagerConfig {
Expand Down

0 comments on commit b574cbf

Please sign in to comment.