Skip to content

Commit

Permalink
testing(store): add basic store testing
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfriesen committed Nov 19, 2024
1 parent 5e257e0 commit c816307
Show file tree
Hide file tree
Showing 12 changed files with 1,419 additions and 619 deletions.
22 changes: 18 additions & 4 deletions apps/timer/app/stores/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,26 @@ interface EntityStoreState<T> {
}

export function useEntityStore<T extends { id: string }>(name: string) {
return defineStore(name, () => {
return defineStore(name, function () {
const state = ref<EntityStoreState<T>>({
entities: {},
ids: [],
});

function getState() {
return state.value;
}
function setState(data: EntityStoreState<T>) {
state.value = data;
}

function upsertMany(entities: T[]) {
for (const entity of entities) {
const id = entity.id;
if (!id) {
console.warn('entity must have an id', entity);
if (import.meta.env.DEV) {
console.warn('entity must have an id', entity);
}
continue;
}
state.value.entities[id] = entity;
Expand All @@ -32,7 +41,9 @@ export function useEntityStore<T extends { id: string }>(name: string) {

function insert(entity: T) {
if (!entity?.['id']) {
console.warn('entity must have an id', entity);
if (import.meta.env.DEV) {
console.warn('entity must have an id', entity);
}
return;
}
const id = entity.id;
Expand Down Expand Up @@ -85,6 +96,9 @@ export function useEntityStore<T extends { id: string }>(name: string) {
}

return {
getState,
setState,

upsertMany,

insert,
Expand All @@ -95,5 +109,5 @@ export function useEntityStore<T extends { id: string }>(name: string) {
getAll,
find,
};
});
})();
}
2 changes: 1 addition & 1 deletion apps/timer/app/stores/settingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const useSettingsStore = defineStore('settings', () => {

const themeMode = computed({
get() {
return useColorMode().value
return useColorMode().value || defaultSettings.themeMode;
},
set(option) {
useColorMode().preference = option
Expand Down
29 changes: 22 additions & 7 deletions apps/timer/app/stores/timerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,27 @@ export const useTimerStore = defineStore('timer', () => {
// Draft
const draft = ref<DraftTime | null>(null);
const isStarted = computed(() => !!draft.value);
function updateDraft(data: DraftTime) {
function setDraft(data: DraftTime) {
draft.value = data;
return draft;
}
function updateDraft(data: Partial<DraftTime>) {
if (!draft.value) {
if (import.meta.env.DEV) {
console.warn('Draft is not started');
}
return draft.value;
};

draft.value = {
...draft.value,
...data
};

return draft;
}
function startDraft(time?: Partial<DraftTime>) {
updateDraft({
setDraft({
start: new Date().toISOString(),
notes: '',
...time,
Expand All @@ -34,7 +49,7 @@ export const useTimerStore = defineStore('timer', () => {

const time = { id: nanoid(), end: new Date().toISOString(), ...draftValue };

timesStore().insert(time);
timesStore.insert(time);
resetDraft();

return time;
Expand All @@ -53,14 +68,14 @@ export const useTimerStore = defineStore('timer', () => {
const times = useLocalStorage().getItem<Time[]>('times');
if (!times) return;

timesStore().upsertMany(times);
timesStore.upsertMany(times);
}

function loadDraftFromLocalStorage() {
const value = useLocalStorage().getItem<DraftTime>('draft');
if (!value) return;

updateDraft(value);
setDraft(value);
}

loadTimesFromLocalStorage();
Expand All @@ -69,12 +84,12 @@ export const useTimerStore = defineStore('timer', () => {
watch(draft, (value) => {
useLocalStorage().setItem('draft', value);
});
watch(timesStore().getAll(), (value) => {
watch(timesStore.getAll(), (value) => {
useLocalStorage().setItem('times', value);
});

return {
times: timesStore(),
times: timesStore,

draft,
isStarted,
Expand Down
1 change: 1 addition & 0 deletions apps/timer/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default defineNuxtConfig({
compatibilityVersion: 4,
},
modules: [
'@nuxt/test-utils/module',
'@nuxt/eslint',
'@pinia/nuxt',
'@nuxt/ui',
Expand Down
8 changes: 6 additions & 2 deletions apps/timer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.14",
"@nuxt/eslint": "^0.6.1",
"@iconify-json/lucide": "^1.2.15",
"@nuxt/eslint": "^0.6.2",
"@nuxt/image": "^1.8.1",
"@nuxt/test-utils": "^3.14.4",
"@nuxt/ui": "next",
"@vitest/coverage-v8": "^2.1.5",
"@vue/test-utils": "^2.4.6",
"happy-dom": "^15.11.6",
"nuxt": "^3.14.159",
"nuxt-time": "^1.0.2"
}
Expand Down
162 changes: 162 additions & 0 deletions apps/timer/test/store/entityStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';

import { useEntityStore } from '../../app/stores/entityStore';

describe('entityStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
});

it('should be defined', () => {
const store = useEntityStore('test');
expect(store).toBeDefined();
});

it('should have a default state', () => {
const store = useEntityStore('test');
expect(store.getState()).toEqual({ entities: {}, ids: [] });
});

describe('upsertMany', () => {
it('should insert many entities', () => {
const store = useEntityStore('test');
store.upsertMany([{ id: '1' }, { id: '2' }]);

expect(store.getState()).toEqual({
entities: { '1': { id: '1' }, '2': { id: '2' } },
ids: ['1', '2'],
});
});

it('should update existing entities', () => {
const store = useEntityStore<{ id: string, name: string }>('test');
store.insert({ id: '1', name: 'empty' });
store.upsertMany([{ id: '1', name: 'test' }]);

expect(store.getState()).toEqual({
entities: { '1': { id: '1', name: 'test' } },
ids: ['1'],
});
});

it('should not insert entities without an id', () => {
const store = useEntityStore('test');
// @ts-expect-error: id is missing
store.upsertMany([{}, {}]);

expect(store.getState()).toEqual({ entities: {}, ids: [] });
});
});

describe('insert', () => {
it('should insert an entity', () => {
const store = useEntityStore('test');
store.insert({ id: '1' });

expect(store.getState()).toEqual({
entities: { '1': { id: '1' } },
ids: ['1'],
});
});


it('should not insert an entity without an id', () => {
const store = useEntityStore('test');
// @ts-expect-error: id is missing
store.insert({});

expect(store.getState()).toEqual({ entities: {}, ids: [] });
});
});

describe('update', () => {
it('should update an entity', () => {
const store = useEntityStore<{ id: string, name: string }>('test');
store.insert({ id: '1', name: 'test' });
store.update('1', { name: 'updated' });

expect(store.getState()).toEqual({
entities: { '1': { id: '1', name: 'updated' } },
ids: ['1'],
});
});


it('should not update an entity that does not exist', () => {
const store = useEntityStore('test');
store.update('1', {});

expect(store.getState()).toEqual({ entities: {}, ids: [] });
});
});

describe('remove', () => {
it('should remove an entity', () => {
const store = useEntityStore('test');
store.insert({ id: '1' });
store.remove('1');

expect(store.getState()).toEqual({ entities: {}, ids: [] });
});

it('should not remove an entity that does not exist', () => {
const store = useEntityStore('test');
store.remove('1');

expect(store.getState()).toEqual({ entities: {}, ids: [] });
});
});

describe('getById', () => {
it('should get an entity by id', () => {
const store = useEntityStore<{ id: string, name: string }>('test');
store.insert({ id: '1', name: 'test' });

expect(store.getById('1').value).toEqual({ id: '1', name: 'test' });
});

it('should return undefined if the entity does not exist', () => {
const store = useEntityStore('test');

expect(store.getById('1').value).toBeUndefined();
});
});

describe('getAll', () => {
it('should get entities by ids', () => {
const store = useEntityStore<{ id: string, name: string }>('test');
store.insert({ id: '1', name: 'test' });
store.insert({ id: '2', name: 'test' });

expect(store.getAll().value).toEqual([
{ id: '1', name: 'test' },
{ id: '2', name: 'test' },
]);
});

it('should return an empty array if there are no entities', () => {
const store = useEntityStore('test');

expect(store.getAll().value).toEqual([]);
});
});

describe('find', () => {
it('should find entities by predicate', () => {
const store = useEntityStore<{ id: string, name: string }>('test');
store.insert({ id: '1', name: 'test 1' });
store.insert({ id: '2', name: 'test 2' });

expect(store.find((entity) => entity.name === 'test 1').value).toEqual([
{ id: '1', name: 'test 1' },
]);
});

it('should return an empty array if there are no entities', () => {
const store = useEntityStore('test');

expect(store.find(() => true).value).toEqual([]);
});
});
});
62 changes: 62 additions & 0 deletions apps/timer/test/store/settingsStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
import { mockNuxtImport } from '@nuxt/test-utils/runtime';

import { useSettingsStore } from '../../app/stores/settingsStore';

const { useColorMode } = vi.hoisted(() => ({
useColorMode: () => ({
value: 'dark',
preference: 'system',
}),
}));
mockNuxtImport('useColorMode', () => useColorMode);

describe("useSettingsStore", () => {
beforeEach(() => {
vi.resetAllMocks();
setActivePinia(createPinia());
});

it('should have a default state', () => {
const store = useSettingsStore();
expect(store.settings).toStrictEqual({ language: 'en', themeMode: 'dark', themePrimary: 'sky' });
});

describe('localStorage', () => {
it('should load settings from localStorage', () => {
vi.spyOn(localStorage, 'getItem').mockReturnValue((JSON.stringify({ language: 'fr', themeMode: 'dark', themePrimary: 'red' })));

const store = useSettingsStore();
expect(store.settings).toEqual({ language: 'fr', themeMode: 'dark', themePrimary: 'red' });
});

it('should save settings to localStorage', async () => {
const spy = vi.spyOn(localStorage, 'setItem');
const store = useSettingsStore();
store.updateSettings({ language: 'fr' });

await nextTick();

expect(spy).toHaveBeenCalledWith('settings', JSON.stringify({ language: 'fr', themeMode: 'dark', themePrimary: 'sky' }));
});
});

describe('updateSettings', () => {
it('should update settings', () => {
const store = useSettingsStore();
store.updateSettings({ language: 'fr' });

expect(store.settings).toStrictEqual({ language: 'fr', themeMode: 'dark', themePrimary: 'sky' });
});
});

describe('setLanguage', () => {
it('should set the language', () => {
const store = useSettingsStore();
store.setLanguage('fr');

expect(store.settings.language).toStrictEqual('fr');
});
});
});
Loading

0 comments on commit c816307

Please sign in to comment.