Skip to content

Commit 5bcae4d

Browse files
committed
test(ui): add tests for bible reader sign in/out
1 parent 64c0e6e commit 5bcae4d

File tree

3 files changed

+236
-2
lines changed

3 files changed

+236
-2
lines changed

packages/ui/src/components/bible-reader.stories.tsx

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
11
import type { Meta, StoryObj } from '@storybook/react-vite';
2-
import { expect, screen, userEvent, waitFor } from 'storybook/test';
2+
import { expect, fn, screen, spyOn, userEvent, waitFor } from 'storybook/test';
33
import { BibleReader } from './bible-reader';
4+
import { setupAuthenticatedUser } from '../test/utils';
5+
6+
let signInMock: ReturnType<typeof fn>;
47

58
const meta: Meta<typeof BibleReader.Root> = {
69
title: 'Components/BibleReader',
710
component: BibleReader.Root,
811
parameters: {
912
layout: 'fullscreen',
1013
},
11-
beforeEach: () => {
14+
beforeEach: async () => {
1215
localStorage.clear();
16+
17+
const { YouVersionAPIUsers } = await import('@youversion/platform-core');
18+
19+
signInMock = fn().mockImplementation(async () => {
20+
await new Promise<void>((resolve) => setTimeout(resolve, 100));
21+
return {
22+
accessToken: 'mock-token',
23+
errorMsg: null,
24+
yvpUserId: 'mock-user-id',
25+
};
26+
});
27+
28+
spyOn(YouVersionAPIUsers, 'signIn')
29+
.mockImplementation(signInMock)
30+
.mockName('YouVersionAPIUsers.signIn');
1331
},
1432
argTypes: {
1533
versionId: {
@@ -340,3 +358,182 @@ export const ThemeOverridesProvider: Story = {
340358
});
341359
},
342360
};
361+
362+
export const SignInFlow: Story = {
363+
tags: ['integration'],
364+
args: {
365+
versionId: 111,
366+
book: 'JHN',
367+
chapter: '1',
368+
background: 'light',
369+
},
370+
render: (args) => (
371+
<div className="yv:h-screen yv:bg-background">
372+
<BibleReader.Root {...args}>
373+
<BibleReader.Content />
374+
<BibleReader.Toolbar />
375+
</BibleReader.Root>
376+
</div>
377+
),
378+
play: async () => {
379+
await waitFor(
380+
async () => {
381+
const userMenuTrigger = screen.getByTestId('user-menu-trigger');
382+
await expect(userMenuTrigger).toBeInTheDocument();
383+
},
384+
{ timeout: 5000 },
385+
);
386+
387+
const userMenuTrigger = screen.getByTestId('user-menu-trigger');
388+
await expect(userMenuTrigger.querySelector('img')).not.toBeInTheDocument();
389+
390+
await userEvent.click(userMenuTrigger);
391+
392+
await waitFor(async () => {
393+
const signInButton = await screen.findByRole('button', { name: /sign in/i });
394+
await expect(signInButton).toBeInTheDocument();
395+
});
396+
397+
const signInButton = screen.getByRole('button', { name: /sign in/i });
398+
await userEvent.click(signInButton);
399+
400+
await expect(signInMock).toHaveBeenCalled();
401+
},
402+
};
403+
404+
export const SignOutFlow: Story = {
405+
tags: ['integration'],
406+
args: {
407+
versionId: 111,
408+
book: 'JHN',
409+
chapter: '1',
410+
background: 'light',
411+
},
412+
beforeEach: async () => {
413+
localStorage.clear();
414+
await setupAuthenticatedUser({
415+
avatarUrl: 'https://example.com/avatar/{width}/{height}.jpg',
416+
});
417+
},
418+
render: (args) => (
419+
<div className="yv:h-screen yv:bg-background">
420+
<BibleReader.Root {...args}>
421+
<BibleReader.Content />
422+
<BibleReader.Toolbar />
423+
</BibleReader.Root>
424+
</div>
425+
),
426+
play: async () => {
427+
await waitFor(
428+
async () => {
429+
const userMenuTrigger = screen.getByTestId('user-menu-trigger');
430+
await expect(userMenuTrigger).toBeInTheDocument();
431+
},
432+
{ timeout: 5000 },
433+
);
434+
435+
const userMenuTrigger = screen.getByTestId('user-menu-trigger');
436+
437+
await waitFor(async () => {
438+
const avatar = userMenuTrigger.querySelector('img');
439+
await expect(avatar).toBeInTheDocument();
440+
});
441+
442+
await userEvent.click(userMenuTrigger);
443+
444+
await waitFor(async () => {
445+
const signOutButton = await screen.findByRole('button', { name: /sign out/i });
446+
await expect(signOutButton).toBeInTheDocument();
447+
});
448+
449+
const signOutButton = screen.getByRole('button', { name: /sign out/i });
450+
await userEvent.click(signOutButton);
451+
452+
await waitFor(async () => {
453+
const userMenuTriggerAfterSignOut = screen.getByTestId('user-menu-trigger');
454+
await expect(userMenuTriggerAfterSignOut.querySelector('img')).not.toBeInTheDocument();
455+
});
456+
},
457+
};
458+
459+
export const AuthenticatedWithAvatar: Story = {
460+
tags: ['integration'],
461+
args: {
462+
versionId: 111,
463+
book: 'JHN',
464+
chapter: '1',
465+
background: 'light',
466+
},
467+
beforeEach: async () => {
468+
localStorage.clear();
469+
await setupAuthenticatedUser({
470+
avatarUrl: 'https://example.com/avatar/{width}/{height}.jpg',
471+
});
472+
},
473+
render: (args) => (
474+
<div className="yv:h-screen yv:bg-background">
475+
<BibleReader.Root {...args}>
476+
<BibleReader.Content />
477+
<BibleReader.Toolbar />
478+
</BibleReader.Root>
479+
</div>
480+
),
481+
play: async () => {
482+
await waitFor(
483+
async () => {
484+
const userMenuTrigger = screen.getByTestId('user-menu-trigger');
485+
await expect(userMenuTrigger).toBeInTheDocument();
486+
},
487+
{ timeout: 5000 },
488+
);
489+
490+
const userMenuTrigger = screen.getByTestId('user-menu-trigger');
491+
492+
await waitFor(async () => {
493+
const avatar = userMenuTrigger.querySelector('img');
494+
await expect(avatar).toBeInTheDocument();
495+
await expect(avatar?.getAttribute('src')).toContain('example.com/avatar');
496+
});
497+
},
498+
};
499+
500+
export const AuthenticatedWithoutAvatar: Story = {
501+
tags: ['integration'],
502+
args: {
503+
versionId: 111,
504+
book: 'JHN',
505+
chapter: '1',
506+
background: 'light',
507+
},
508+
beforeEach: async () => {
509+
localStorage.clear();
510+
await setupAuthenticatedUser();
511+
},
512+
render: (args) => (
513+
<div className="yv:h-screen yv:bg-background">
514+
<BibleReader.Root {...args}>
515+
<BibleReader.Content />
516+
<BibleReader.Toolbar />
517+
</BibleReader.Root>
518+
</div>
519+
),
520+
play: async () => {
521+
await waitFor(
522+
async () => {
523+
const userMenuTrigger = screen.getByTestId('user-menu-trigger');
524+
await expect(userMenuTrigger).toBeInTheDocument();
525+
},
526+
{ timeout: 5000 },
527+
);
528+
529+
const userMenuTrigger = screen.getByTestId('user-menu-trigger');
530+
await expect(userMenuTrigger.querySelector('img')).not.toBeInTheDocument();
531+
532+
await userEvent.click(userMenuTrigger);
533+
534+
await waitFor(async () => {
535+
const signOutButton = await screen.findByRole('button', { name: /sign out/i });
536+
await expect(signOutButton).toBeInTheDocument();
537+
});
538+
},
539+
};

packages/ui/src/components/bible-reader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ function Toolbar({ border = 'top' }: { border?: 'top' | 'bottom' }) {
247247
<div className="yv:grid yv:w-full yv:grid-cols-7 yv:items-center yv:max-w-lg yv:gap-0.5">
248248
<Popover>
249249
<PopoverTrigger
250+
data-testid="user-menu-trigger"
250251
className={cn(
251252
'yv:inline-flex yv:items-center yv:justify-center yv:justify-self-end yv:mr-4 yv:h-9 yv:rounded-md yv:bg-muted yv:text-foreground yv:hover:bg-muted/80 yv:hover:cursor-pointer yv:overflow-hidden',
252253
!(auth.isAuthenticated && userInfo?.avatarUrlFormat) && 'yv:px-2',

packages/ui/src/test/utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { YouVersionUserInfo } from '@youversion/platform-core';
2+
import { spyOn } from 'storybook/test';
3+
4+
export type MockAuthUserOptions = {
5+
id?: string;
6+
name?: string;
7+
email?: string;
8+
avatarUrl?: string | null;
9+
};
10+
11+
export async function setupAuthenticatedUser(
12+
options: MockAuthUserOptions = {},
13+
): Promise<YouVersionUserInfo> {
14+
const { YouVersionAPIUsers, YouVersionPlatformConfiguration, YouVersionUserInfo } = await import(
15+
'@youversion/platform-core'
16+
);
17+
18+
YouVersionPlatformConfiguration.saveAuthData(
19+
'mock-access-token',
20+
'mock-refresh-token',
21+
'mock-id-token',
22+
null,
23+
);
24+
25+
const mockUserInfo = new YouVersionUserInfo({
26+
id: options.id ?? 'mock-user-id',
27+
name: options.name ?? 'Test User',
28+
email: options.email ?? '[email protected]',
29+
avatar_url: options.avatarUrl ?? undefined,
30+
});
31+
32+
spyOn(YouVersionAPIUsers, 'refreshTokenIfNeeded').mockResolvedValue(false);
33+
spyOn(YouVersionAPIUsers, 'userInfo').mockReturnValue(mockUserInfo);
34+
35+
return mockUserInfo;
36+
}

0 commit comments

Comments
 (0)