diff options
Diffstat (limited to '__tests__/state/models')
-rw-r--r-- | __tests__/state/models/link-metas-view.test.ts | 72 | ||||
-rw-r--r-- | __tests__/state/models/log.test.ts | 153 | ||||
-rw-r--r-- | __tests__/state/models/me.test.ts | 183 | ||||
-rw-r--r-- | __tests__/state/models/navigation.test.ts | 154 | ||||
-rw-r--r-- | __tests__/state/models/onboard.test.ts | 46 | ||||
-rw-r--r-- | __tests__/state/models/root-store.test.ts | 73 | ||||
-rw-r--r-- | __tests__/state/models/shell-ui.test.ts | 59 |
7 files changed, 740 insertions, 0 deletions
diff --git a/__tests__/state/models/link-metas-view.test.ts b/__tests__/state/models/link-metas-view.test.ts new file mode 100644 index 000000000..037418932 --- /dev/null +++ b/__tests__/state/models/link-metas-view.test.ts @@ -0,0 +1,72 @@ +import {RootStoreModel} from '../../../src/state/models/root-store' +import {LinkMetasViewModel} from '../../../src/state/models/link-metas-view' +import * as LinkMetaLib from '../../../src/lib/link-meta' +import {LikelyType} from './../../../src/lib/link-meta' +import {sessionClient, SessionServiceClient} from '@atproto/api' +import {DEFAULT_SERVICE} from '../../../src/state' + +describe('LinkMetasViewModel', () => { + let viewModel: LinkMetasViewModel + let rootStore: RootStoreModel + + const getLinkMetaMockSpy = jest.spyOn(LinkMetaLib, 'getLinkMeta') + const mockedMeta = { + title: 'Test Title', + url: 'testurl', + likelyType: LikelyType.Other, + } + + beforeEach(() => { + const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient + rootStore = new RootStoreModel(api) + viewModel = new LinkMetasViewModel(rootStore) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + describe('getLinkMeta', () => { + it('should return link meta if it is cached', async () => { + const url = 'http://example.com' + + viewModel.cache.set(url, mockedMeta) + + const result = await viewModel.getLinkMeta(url) + + expect(getLinkMetaMockSpy).not.toHaveBeenCalled() + expect(result).toEqual(mockedMeta) + }) + + it('should return link meta if it is not cached', async () => { + getLinkMetaMockSpy.mockResolvedValueOnce(mockedMeta) + + const result = await viewModel.getLinkMeta(mockedMeta.url) + + expect(getLinkMetaMockSpy).toHaveBeenCalledWith(mockedMeta.url) + expect(result).toEqual(mockedMeta) + }) + + it('should cache the link meta if it is successfully returned', async () => { + getLinkMetaMockSpy.mockResolvedValueOnce(mockedMeta) + + await viewModel.getLinkMeta(mockedMeta.url) + + expect(viewModel.cache.get(mockedMeta.url)).toEqual(mockedMeta) + }) + + it('should not cache the link meta if it fails to return', async () => { + const url = 'http://example.com' + const error = new Error('Failed to fetch link meta') + getLinkMetaMockSpy.mockRejectedValueOnce(error) + + try { + await viewModel.getLinkMeta(url) + fail('Error was not thrown') + } catch (e) { + expect(e).toEqual(error) + expect(viewModel.cache.get(url)).toBeUndefined() + } + }) + }) +}) diff --git a/__tests__/state/models/log.test.ts b/__tests__/state/models/log.test.ts new file mode 100644 index 000000000..b5a6d0db0 --- /dev/null +++ b/__tests__/state/models/log.test.ts @@ -0,0 +1,153 @@ +import {LogModel} from '../../../src/state/models/log' + +describe('LogModel', () => { + let logModel: LogModel + + beforeEach(() => { + logModel = new LogModel() + jest.spyOn(console, 'debug') + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it('should call a log method and add a log entry to the entries array', () => { + logModel.debug('Test log') + expect(logModel.entries.length).toEqual(1) + expect(logModel.entries[0]).toEqual({ + id: logModel.entries[0].id, + type: 'debug', + summary: 'Test log', + details: undefined, + ts: logModel.entries[0].ts, + }) + + logModel.warn('Test log') + expect(logModel.entries.length).toEqual(2) + expect(logModel.entries[1]).toEqual({ + id: logModel.entries[1].id, + type: 'warn', + summary: 'Test log', + details: undefined, + ts: logModel.entries[1].ts, + }) + + logModel.error('Test log') + expect(logModel.entries.length).toEqual(3) + expect(logModel.entries[2]).toEqual({ + id: logModel.entries[2].id, + type: 'error', + summary: 'Test log', + details: undefined, + ts: logModel.entries[2].ts, + }) + }) + + it('should call the console.debug after calling the debug method', () => { + logModel.debug('Test log') + expect(console.debug).toHaveBeenCalledWith('Test log', '') + }) + + it('should call the serialize method', () => { + logModel.debug('Test log') + expect(logModel.serialize()).toEqual({ + entries: [ + { + id: logModel.entries[0].id, + type: 'debug', + summary: 'Test log', + details: undefined, + ts: logModel.entries[0].ts, + }, + ], + }) + }) + + it('should call the hydrate method with valid properties', () => { + logModel.hydrate({ + entries: [ + { + id: '123', + type: 'debug', + summary: 'Test log', + details: undefined, + ts: 123, + }, + ], + }) + expect(logModel.entries).toEqual([ + { + id: '123', + type: 'debug', + summary: 'Test log', + details: undefined, + ts: 123, + }, + ]) + }) + + it('should call the hydrate method with invalid properties', () => { + logModel.hydrate({ + entries: [ + { + id: '123', + type: 'debug', + summary: 'Test log', + details: undefined, + ts: 123, + }, + { + summary: 'Invalid entry', + }, + ], + }) + expect(logModel.entries).toEqual([ + { + id: '123', + type: 'debug', + summary: 'Test log', + details: undefined, + ts: 123, + }, + ]) + }) + + it('should stringify the details if it is not a string', () => { + logModel.debug('Test log', {details: 'test'}) + expect(logModel.entries[0].details).toEqual('{\n "details": "test"\n}') + }) + + it('should stringify the details object if it is of a specific error', () => { + class TestError extends Error { + constructor() { + super() + this.name = 'TestError' + } + } + const error = new TestError() + logModel.error('Test error log', error) + expect(logModel.entries[0].details).toEqual('TestError') + + class XRPCInvalidResponseErrorMock { + validationError = {toString: () => 'validationError'} + lexiconNsid = 'test' + } + const xrpcInvalidResponseError = new XRPCInvalidResponseErrorMock() + logModel.error('Test error log', xrpcInvalidResponseError) + expect(logModel.entries[1].details).toEqual( + '{\n "validationError": {},\n "lexiconNsid": "test"\n}', + ) + + class XRPCErrorMock { + status = 'status' + error = 'error' + message = 'message' + } + const xrpcError = new XRPCErrorMock() + logModel.error('Test error log', xrpcError) + expect(logModel.entries[2].details).toEqual( + '{\n "status": "status",\n "error": "error",\n "message": "message"\n}', + ) + }) +}) diff --git a/__tests__/state/models/me.test.ts b/__tests__/state/models/me.test.ts new file mode 100644 index 000000000..a1ffa3fbe --- /dev/null +++ b/__tests__/state/models/me.test.ts @@ -0,0 +1,183 @@ +import {RootStoreModel} from '../../../src/state/models/root-store' +import {MeModel} from '../../../src/state/models/me' +import {MembershipsViewModel} from './../../../src/state/models/memberships-view' +import {NotificationsViewModel} from './../../../src/state/models/notifications-view' +import {sessionClient, SessionServiceClient} from '@atproto/api' +import {DEFAULT_SERVICE} from './../../../src/state/index' + +describe('MeModel', () => { + let rootStore: RootStoreModel + let meModel: MeModel + + beforeEach(() => { + const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient + rootStore = new RootStoreModel(api) + meModel = new MeModel(rootStore) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it('should clear() correctly', () => { + meModel.did = '123' + meModel.handle = 'handle' + meModel.displayName = 'John Doe' + meModel.description = 'description' + meModel.avatar = 'avatar' + meModel.notificationCount = 1 + meModel.clear() + expect(meModel.did).toEqual('') + expect(meModel.handle).toEqual('') + expect(meModel.displayName).toEqual('') + expect(meModel.description).toEqual('') + expect(meModel.avatar).toEqual('') + expect(meModel.notificationCount).toEqual(0) + expect(meModel.memberships).toBeUndefined() + }) + + it('should hydrate() successfully with valid properties', () => { + meModel.hydrate({ + did: '123', + handle: 'handle', + displayName: 'John Doe', + description: 'description', + avatar: 'avatar', + }) + expect(meModel.did).toEqual('123') + expect(meModel.handle).toEqual('handle') + expect(meModel.displayName).toEqual('John Doe') + expect(meModel.description).toEqual('description') + expect(meModel.avatar).toEqual('avatar') + }) + + it('should not hydrate() with invalid properties', () => { + meModel.hydrate({ + did: '', + handle: 'handle', + displayName: 'John Doe', + description: 'description', + avatar: 'avatar', + }) + expect(meModel.did).toEqual('') + expect(meModel.handle).toEqual('') + expect(meModel.displayName).toEqual('') + expect(meModel.description).toEqual('') + expect(meModel.avatar).toEqual('') + + meModel.hydrate({ + did: '123', + displayName: 'John Doe', + description: 'description', + avatar: 'avatar', + }) + expect(meModel.did).toEqual('') + expect(meModel.handle).toEqual('') + expect(meModel.displayName).toEqual('') + expect(meModel.description).toEqual('') + expect(meModel.avatar).toEqual('') + }) + + it('should load() successfully', async () => { + jest + .spyOn(rootStore.api.app.bsky.actor, 'getProfile') + .mockImplementationOnce((): Promise<any> => { + return Promise.resolve({ + data: { + displayName: 'John Doe', + description: 'description', + avatar: 'avatar', + }, + }) + }) + rootStore.session.data = { + did: '123', + handle: 'handle', + service: 'test service', + accessJwt: 'test token', + refreshJwt: 'test token', + } + await meModel.load() + expect(meModel.did).toEqual('123') + expect(meModel.handle).toEqual('handle') + expect(meModel.displayName).toEqual('John Doe') + expect(meModel.description).toEqual('description') + expect(meModel.avatar).toEqual('avatar') + }) + + it('should load() successfully without profile data', async () => { + jest + .spyOn(rootStore.api.app.bsky.actor, 'getProfile') + .mockImplementationOnce((): Promise<any> => { + return Promise.resolve({ + data: null, + }) + }) + rootStore.session.data = { + did: '123', + handle: 'handle', + service: 'test service', + accessJwt: 'test token', + refreshJwt: 'test token', + } + await meModel.load() + expect(meModel.did).toEqual('123') + expect(meModel.handle).toEqual('handle') + expect(meModel.displayName).toEqual('') + expect(meModel.description).toEqual('') + expect(meModel.avatar).toEqual('') + }) + + it('should load() to nothing when no session', async () => { + rootStore.session.data = null + await meModel.load() + expect(meModel.did).toEqual('') + expect(meModel.handle).toEqual('') + expect(meModel.displayName).toEqual('') + expect(meModel.description).toEqual('') + expect(meModel.avatar).toEqual('') + expect(meModel.notificationCount).toEqual(0) + expect(meModel.memberships).toBeUndefined() + }) + + it('should serialize() key information', () => { + meModel.did = '123' + meModel.handle = 'handle' + meModel.displayName = 'John Doe' + meModel.description = 'description' + meModel.avatar = 'avatar' + + expect(meModel.serialize()).toEqual({ + did: '123', + handle: 'handle', + displayName: 'John Doe', + description: 'description', + avatar: 'avatar', + }) + }) + + it('should clearNotificationCount() successfully', () => { + meModel.clearNotificationCount() + expect(meModel.notificationCount).toBe(0) + }) + + it('should update notifs count with fetchStateUpdate()', async () => { + meModel.notifications = { + refresh: jest.fn(), + } as unknown as NotificationsViewModel + + jest + .spyOn(rootStore.api.app.bsky.notification, 'getCount') + .mockImplementationOnce((): Promise<any> => { + return Promise.resolve({ + data: { + count: 1, + }, + }) + }) + + await meModel.fetchStateUpdate() + expect(meModel.notificationCount).toBe(1) + expect(meModel.notifications.refresh).toHaveBeenCalled() + }) +}) diff --git a/__tests__/state/models/navigation.test.ts b/__tests__/state/models/navigation.test.ts new file mode 100644 index 000000000..bc49d2ee3 --- /dev/null +++ b/__tests__/state/models/navigation.test.ts @@ -0,0 +1,154 @@ +import { + NavigationModel, + NavigationTabModel, +} from './../../../src/state/models/navigation' +import * as flags from '../../../src/build-flags' + +describe('NavigationModel', () => { + let model: NavigationModel + + beforeEach(() => { + model = new NavigationModel() + model.setTitle([0, 0], 'title') + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it('should clear() to the correct base state', async () => { + await model.clear() + expect(model.tabCount).toBe(2) + expect(model.tab).toEqual({ + fixedTabPurpose: 0, + history: [ + { + id: expect.anything(), + ts: expect.anything(), + url: '/', + }, + ], + id: expect.anything(), + index: 0, + isNewTab: false, + }) + }) + + it('should call the navigate method', async () => { + const navigateSpy = jest.spyOn(model.tab, 'navigate') + await model.navigate('testurl', 'teststring') + expect(navigateSpy).toHaveBeenCalledWith('testurl', 'teststring') + }) + + it('should call the refresh method', async () => { + const refreshSpy = jest.spyOn(model.tab, 'refresh') + await model.refresh() + expect(refreshSpy).toHaveBeenCalled() + }) + + it('should call the isCurrentScreen method', () => { + expect(model.isCurrentScreen(11, 0)).toEqual(false) + }) + + it('should call the tab getter', () => { + expect(model.tab).toEqual({ + fixedTabPurpose: 0, + history: [ + { + id: expect.anything(), + ts: expect.anything(), + url: '/', + }, + ], + id: expect.anything(), + index: 0, + isNewTab: false, + }) + }) + + it('should call the tabCount getter', () => { + expect(model.tabCount).toBe(2) + }) + + describe('tabs not enabled', () => { + jest.mock('../../../src/build-flags', () => ({ + TABS_ENABLED: false, + })) + + afterAll(() => { + jest.clearAllMocks() + }) + + it('should not create new tabs', () => { + // @ts-expect-error + flags.TABS_ENABLED = false + model.newTab('testurl') + expect(model.tab.isNewTab).toBe(false) + expect(model.tabIndex).toBe(0) + }) + + it('should not change the active tab', () => { + // @ts-expect-error + flags.TABS_ENABLED = false + model.setActiveTab(2) + expect(model.tabIndex).toBe(0) + }) + + it('should note close tabs', () => { + // @ts-expect-error + flags.TABS_ENABLED = false + model.closeTab(0) + expect(model.tabCount).toBe(2) + }) + }) + + describe('tabs enabled', () => { + jest.mock('../../../src/build-flags', () => ({ + TABS_ENABLED: true, + })) + + afterAll(() => { + jest.clearAllMocks() + }) + + it('should create new tabs', () => { + // @ts-expect-error + flags.TABS_ENABLED = true + + model.newTab('testurl', 'title') + expect(model.tab.isNewTab).toBe(true) + expect(model.tabIndex).toBe(2) + }) + + it('should change the current tab', () => { + // @ts-expect-error + flags.TABS_ENABLED = true + + model.setActiveTab(0) + expect(model.tabIndex).toBe(0) + }) + + it('should close tabs', () => { + // @ts-expect-error + flags.TABS_ENABLED = true + + model.closeTab(0) + expect(model.tabs).toEqual([ + { + fixedTabPurpose: 1, + history: [ + { + id: expect.anything(), + ts: expect.anything(), + url: '/notifications', + }, + ], + id: expect.anything(), + index: 0, + isNewTab: false, + }, + ]) + expect(model.tabIndex).toBe(0) + }) + }) +}) diff --git a/__tests__/state/models/onboard.test.ts b/__tests__/state/models/onboard.test.ts new file mode 100644 index 000000000..02ee0feb6 --- /dev/null +++ b/__tests__/state/models/onboard.test.ts @@ -0,0 +1,46 @@ +import { + OnboardModel, + OnboardStageOrder, +} from '../../../src/state/models/onboard' + +describe('OnboardModel', () => { + let onboardModel: OnboardModel + + beforeEach(() => { + onboardModel = new OnboardModel() + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it('should start/stop correctly', () => { + onboardModel.start() + expect(onboardModel.isOnboarding).toBe(true) + onboardModel.stop() + expect(onboardModel.isOnboarding).toBe(false) + }) + + it('should call the next method until it has no more stages', () => { + onboardModel.start() + onboardModel.next() + expect(onboardModel.stage).toBe(OnboardStageOrder[1]) + + onboardModel.next() + expect(onboardModel.isOnboarding).toBe(false) + expect(onboardModel.stage).toBe(OnboardStageOrder[0]) + }) + + it('serialize and hydrate', () => { + const serialized = onboardModel.serialize() + const newModel = new OnboardModel() + newModel.hydrate(serialized) + expect(newModel).toEqual(onboardModel) + + onboardModel.start() + onboardModel.next() + const serialized2 = onboardModel.serialize() + newModel.hydrate(serialized2) + expect(newModel).toEqual(onboardModel) + }) +}) diff --git a/__tests__/state/models/root-store.test.ts b/__tests__/state/models/root-store.test.ts new file mode 100644 index 000000000..ccaa6f83f --- /dev/null +++ b/__tests__/state/models/root-store.test.ts @@ -0,0 +1,73 @@ +import {RootStoreModel} from '../../../src/state/models/root-store' +import {setupState} from '../../../src/state' + +describe('rootStore', () => { + let rootStore: RootStoreModel + + beforeAll(() => { + jest.useFakeTimers() + }) + + beforeEach(async () => { + rootStore = await setupState() + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it('resolveName() handles inputs correctly', () => { + const spyMethod = jest + .spyOn(rootStore.api.com.atproto.handle, 'resolve') + .mockResolvedValue({success: true, headers: {}, data: {did: 'testdid'}}) + + rootStore.resolveName('teststring') + expect(spyMethod).toHaveBeenCalledWith({handle: 'teststring'}) + + expect(rootStore.resolveName('')).rejects.toThrow('Invalid handle: ""') + + expect(rootStore.resolveName('did:123')).resolves.toReturnWith('did:123') + }) + + it('should call the clearAll() resets state correctly', () => { + rootStore.clearAll() + + expect(rootStore.session.data).toEqual(null) + expect(rootStore.nav.tabs).toEqual([ + { + fixedTabPurpose: 0, + history: [ + { + id: expect.anything(), + ts: expect.anything(), + url: '/', + }, + ], + id: expect.anything(), + index: 0, + isNewTab: false, + }, + { + fixedTabPurpose: 1, + history: [ + { + id: expect.anything(), + ts: expect.anything(), + url: '/notifications', + }, + ], + id: expect.anything(), + index: 0, + isNewTab: false, + }, + ]) + expect(rootStore.nav.tabIndex).toEqual(0) + expect(rootStore.me.did).toEqual('') + expect(rootStore.me.handle).toEqual('') + expect(rootStore.me.displayName).toEqual('') + expect(rootStore.me.description).toEqual('') + expect(rootStore.me.avatar).toEqual('') + expect(rootStore.me.notificationCount).toEqual(0) + expect(rootStore.me.memberships).toBeUndefined() + }) +}) diff --git a/__tests__/state/models/shell-ui.test.ts b/__tests__/state/models/shell-ui.test.ts new file mode 100644 index 000000000..8324609a1 --- /dev/null +++ b/__tests__/state/models/shell-ui.test.ts @@ -0,0 +1,59 @@ +import { + ConfirmModal, + ImageLightbox, + ShellUiModel, +} from './../../../src/state/models/shell-ui' + +describe('ShellUiModel', () => { + let model: ShellUiModel + + beforeEach(() => { + model = new ShellUiModel() + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it('should call the openModal & closeModal method', () => { + model.openModal(ConfirmModal) + expect(model.isModalActive).toEqual(true) + expect(model.activeModal).toEqual(ConfirmModal) + + model.closeModal() + expect(model.isModalActive).toEqual(false) + expect(model.activeModal).toBeUndefined() + }) + + it('should call the openLightbox & closeLightbox method', () => { + model.openLightbox(new ImageLightbox('uri')) + expect(model.isLightboxActive).toEqual(true) + expect(model.activeLightbox).toEqual(new ImageLightbox('uri')) + + model.closeLightbox() + expect(model.isLightboxActive).toEqual(false) + expect(model.activeLightbox).toBeUndefined() + }) + + it('should call the openComposer & closeComposer method', () => { + const composer = { + replyTo: { + uri: 'uri', + cid: 'cid', + text: 'text', + author: { + handle: 'handle', + displayName: 'name', + }, + }, + onPost: jest.fn(), + } + model.openComposer(composer) + expect(model.isComposerActive).toEqual(true) + expect(model.composerOpts).toEqual(composer) + + model.closeComposer() + expect(model.isComposerActive).toEqual(false) + expect(model.composerOpts).toBeUndefined() + }) +}) |