Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions packages/shared/src/components/filters/MyFeedHeading.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useRouter } from 'next/router';
import { useAuthContext } from '../../contexts/AuthContext';
import { useActiveFeedNameContext } from '../../contexts';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { useActions, useFeedLayout, useViewSize, ViewSize } from '../../hooks';
import { useShortcutsUser } from '../../features/shortcuts/hooks/useShortcutsUser';
import { ActionType } from '../../graphql/actions';
import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed';
import { getHasSeenTags, setHasSeenTags } from '../../lib/feedSettings';
import { SharedFeedPage } from '../utilities';
import MyFeedHeading from './MyFeedHeading';

jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));

jest.mock('../../contexts/AuthContext', () => ({
useAuthContext: jest.fn(),
}));

jest.mock('../../contexts', () => ({
useActiveFeedNameContext: jest.fn(),
}));

jest.mock('../../contexts/SettingsContext', () => ({
useSettingsContext: jest.fn(),
}));

jest.mock('../../hooks', () => ({
useActions: jest.fn(),
useFeedLayout: jest.fn(),
useViewSize: jest.fn(),
ViewSize: {
MobileL: 'mobile',
Laptop: 'laptop',
},
}));

jest.mock('../../features/shortcuts/hooks/useShortcutsUser', () => ({
useShortcutsUser: jest.fn(),
}));

jest.mock('../../hooks/feed/useCustomDefaultFeed', () => ({
__esModule: true,
default: jest.fn(),
}));

jest.mock('../AlertDot', () => ({
AlertDot: ({ className }: { className?: string }) => (
<div data-testid="alert-dot" className={className} />
),
AlertColor: { Bun: 'bg-accent-bun-default' },
}));

jest.mock('../feeds/FeedSettingsButton', () => ({
FeedSettingsButton: ({
children,
onClick,
}: {
children: React.ReactNode;
onClick: () => void;
}) => (
<button type="button" onClick={onClick}>
{children}
</button>
),
}));

jest.mock('../../lib/constants', () => ({
...jest.requireActual('../../lib/constants'),
webappUrl: 'https://app.daily.dev/',
settingsUrl: 'https://app.daily.dev/settings',
}));

jest.mock('../../lib/feedSettings', () => ({
getHasSeenTags: jest.fn(),
setHasSeenTags: jest.fn(),
}));

const mockUseRouter = useRouter as jest.Mock;
const mockUseAuthContext = useAuthContext as jest.Mock;
const mockUseActiveFeedNameContext = useActiveFeedNameContext as jest.Mock;
const mockUseSettingsContext = useSettingsContext as jest.Mock;
const mockUseActions = useActions as jest.Mock;
const mockUseFeedLayout = useFeedLayout as jest.Mock;
const mockUseViewSize = useViewSize as jest.Mock;
const mockUseShortcutsUser = useShortcutsUser as jest.Mock;
const mockUseCustomDefaultFeed = useCustomDefaultFeed as jest.Mock;
const mockGetHasSeenTags = getHasSeenTags as jest.Mock;
const mockSetHasSeenTags = setHasSeenTags as jest.Mock;

const push = jest.fn();
const completeAction = jest.fn();

const renderComponent = () => render(<MyFeedHeading />);

describe('MyFeedHeading', () => {
beforeEach(() => {
push.mockReset();
push.mockResolvedValue(true);
completeAction.mockReset();
completeAction.mockResolvedValue(undefined);
mockGetHasSeenTags.mockReset();
mockGetHasSeenTags.mockReturnValue(null);
mockSetHasSeenTags.mockReset();

mockUseRouter.mockReturnValue({
push,
pathname: '/',
query: {},
});
mockUseAuthContext.mockReturnValue({
user: { id: 'user-1' },
});
mockUseActiveFeedNameContext.mockReturnValue({
feedName: SharedFeedPage.MyFeed,
});
mockUseSettingsContext.mockReturnValue({
toggleShowTopSites: jest.fn(),
});
mockUseActions.mockReturnValue({
completeAction,
checkHasCompleted: jest.fn().mockReturnValue(false),
isActionsFetched: true,
});
mockUseFeedLayout.mockReturnValue({
shouldUseListFeedLayout: false,
});
mockUseViewSize.mockImplementation((size) => size === ViewSize.Laptop);
mockUseShortcutsUser.mockReturnValue({
isOldUserWithNoShortcuts: false,
showToggleShortcuts: false,
});
mockUseCustomDefaultFeed.mockReturnValue({
isCustomDefaultFeed: false,
defaultFeedId: 'user-1',
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('routes the home custom default feed to its edit page', async () => {
mockUseCustomDefaultFeed.mockReturnValue({
isCustomDefaultFeed: true,
defaultFeedId: 'feed-1',
});

renderComponent();

await userEvent.click(
screen.getByRole('button', { name: 'Feed settings' }),
);

expect(push).toHaveBeenCalledWith(
'https://app.daily.dev/feeds/feed-1/edit',
);
});

it('routes the home For you feed to the user edit page with the tags tab open', async () => {
renderComponent();

await userEvent.click(
screen.getByRole('button', { name: 'Feed settings' }),
);

expect(push).toHaveBeenCalledWith(
'https://app.daily.dev/feeds/user-1/edit?dview=tags',
);
});

it('routes the For you feed to the user edit page with the tags tab open', async () => {
mockUseRouter.mockReturnValue({
push,
pathname: '/my-feed',
query: {},
});

renderComponent();

await userEvent.click(
screen.getByRole('button', { name: 'Feed settings' }),
);

expect(push).toHaveBeenCalledWith(
'https://app.daily.dev/feeds/user-1/edit?dview=tags',
);
});

it('routes custom feeds to their slug or id edit page', async () => {
mockUseRouter.mockReturnValue({
push,
pathname: '/feeds/[slugOrId]',
query: { slugOrId: 'feed-2' },
});
mockUseActiveFeedNameContext.mockReturnValue({
feedName: SharedFeedPage.Custom,
});

renderComponent();

await userEvent.click(
screen.getByRole('button', { name: 'Feed settings' }),
);

expect(push).toHaveBeenCalledWith(
'https://app.daily.dev/feeds/feed-2/edit',
);
});

it('shows the tags reminder dot for the For you feed when tags were not seen yet', () => {
mockGetHasSeenTags.mockReturnValue(false);

renderComponent();

expect(screen.getByTestId('alert-dot')).toBeInTheDocument();
});

it('does not show the tags reminder dot for custom feeds', () => {
mockGetHasSeenTags.mockReturnValue(false);
mockUseRouter.mockReturnValue({
push,
pathname: '/feeds/[slugOrId]',
query: { slugOrId: 'feed-2' },
});
mockUseActiveFeedNameContext.mockReturnValue({
feedName: SharedFeedPage.Custom,
});

renderComponent();

expect(screen.queryByTestId('alert-dot')).not.toBeInTheDocument();
});

it('marks tags as seen before navigating from the For you feed settings button', async () => {
mockGetHasSeenTags.mockReturnValue(false);

renderComponent();

await userEvent.click(
screen.getByRole('button', { name: 'Feed settings' }),
);

expect(mockSetHasSeenTags).toHaveBeenCalledWith('user-1', true);
expect(completeAction).toHaveBeenCalledWith(ActionType.HasSeenTags);
expect(push).toHaveBeenCalledWith(
'https://app.daily.dev/feeds/user-1/edit?dview=tags',
);
});
});
84 changes: 69 additions & 15 deletions packages/shared/src/components/filters/MyFeedHeading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/router';
import { FilterIcon, PlusIcon } from '../icons';
import {
Expand All @@ -12,9 +12,13 @@ import { useActions, useFeedLayout, useViewSize, ViewSize } from '../../hooks';
import { ActionType } from '../../graphql/actions';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { FeedSettingsButton } from '../feeds/FeedSettingsButton';
import { AlertColor, AlertDot } from '../AlertDot';
import { FeedSettingsMenu } from '../feeds/FeedSettings/types';
import { useShortcutsUser } from '../../features/shortcuts/hooks/useShortcutsUser';
import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed';
import { useAuthContext } from '../../contexts/AuthContext';
import { settingsUrl, webappUrl } from '../../lib/constants';
import { getHasSeenTags, setHasSeenTags } from '../../lib/feedSettings';
import { SharedFeedPage } from '../utilities';
import { useActiveFeedNameContext } from '../../contexts';

Expand All @@ -26,46 +30,96 @@ function MyFeedHeading({
onOpenFeedFilters,
}: MyFeedHeadingProps): ReactElement {
const { push, pathname, query } = useRouter();
const { completeAction } = useActions();
const { completeAction, checkHasCompleted, isActionsFetched } = useActions();
const { toggleShowTopSites } = useSettingsContext();
const { isOldUserWithNoShortcuts, showToggleShortcuts } = useShortcutsUser();
const isMobile = useViewSize(ViewSize.MobileL);
const { shouldUseListFeedLayout } = useFeedLayout();
const isLaptop = useViewSize(ViewSize.Laptop);
const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed();
const { feedName } = useActiveFeedNameContext();
const { user } = useAuthContext();
const [hasSeenTagsState, setHasSeenTagsState] = useState<boolean | null>(
null,
);

const hasSeenTagsAction =
isActionsFetched && checkHasCompleted(ActionType.HasSeenTags);

const editFeedUrl = useMemo(() => {
if (isCustomDefaultFeed && pathname === '/') {
return `${webappUrl}feeds/${defaultFeedId}/edit`;
}

if (feedName === SharedFeedPage.MyFeed && user?.id) {
return `${webappUrl}feeds/${user.id}/edit?dview=${FeedSettingsMenu.Tags}`;
}

if (feedName === SharedFeedPage.Custom) {
return `${webappUrl}feeds/${query.slugOrId}/edit`;
}

return `${settingsUrl}/feed/general`;
}, [defaultFeedId, feedName, isCustomDefaultFeed, pathname, query]);
}, [defaultFeedId, feedName, isCustomDefaultFeed, pathname, query, user?.id]);

useEffect(() => {
if (!user?.id) {
setHasSeenTagsState(null);
return;
}

if (hasSeenTagsAction) {
setHasSeenTags(user.id, true);
setHasSeenTagsState(true);
return;
}

setHasSeenTagsState(getHasSeenTags(user.id));
}, [hasSeenTagsAction, user?.id]);

const shouldShowTagsReminder =
feedName === SharedFeedPage.MyFeed && hasSeenTagsState === false;

const onClick = useCallback(() => {
if (shouldShowTagsReminder && user?.id) {
setHasSeenTags(user.id, true);
setHasSeenTagsState(true);
completeAction(ActionType.HasSeenTags).catch(() => null);
}

onOpenFeedFilters?.();

return push(editFeedUrl);
}, [editFeedUrl, onOpenFeedFilters, push]);
}, [
completeAction,
editFeedUrl,
onOpenFeedFilters,
push,
shouldShowTagsReminder,
user?.id,
]);

return (
<>
<FeedSettingsButton
onClick={onClick}
size={ButtonSize.Medium}
variant={isLaptop ? ButtonVariant.Float : ButtonVariant.Tertiary}
icon={<FilterIcon />}
iconPosition={
shouldUseListFeedLayout ? ButtonIconPosition.Right : undefined
}
>
{!isMobile ? 'Feed settings' : null}
</FeedSettingsButton>
<div className="relative">
<FeedSettingsButton
onClick={onClick}
size={ButtonSize.Medium}
variant={isLaptop ? ButtonVariant.Float : ButtonVariant.Tertiary}
icon={<FilterIcon />}
iconPosition={
shouldUseListFeedLayout ? ButtonIconPosition.Right : undefined
}
>
{!isMobile ? 'Feed settings' : null}
</FeedSettingsButton>
{shouldShowTagsReminder && (
<AlertDot
color={AlertColor.Bun}
className="pointer-events-none right-2 top-2 border border-background-default"
/>
)}
</div>
{showToggleShortcuts && (
<Button
size={ButtonSize.Medium}
Expand Down
Loading