Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Add 'Install the browser extension' alert + animation #14399

Merged
merged 14 commits into from
Oct 8, 2020
Merged
18 changes: 17 additions & 1 deletion web/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import React, { Suspense } from 'react'
import React, { Suspense, useCallback, useEffect } from 'react'
import { Redirect, Route, RouteComponentProps, Switch, matchPath } from 'react-router'
import { Observable } from 'rxjs'
import { ActivationProps } from '../../shared/src/components/activation/Activation'
Expand Down Expand Up @@ -64,6 +64,7 @@ import { AuthenticatedUser, authRequired as authRequiredObservable } from './aut
import { SearchPatternType } from './graphql-operations'
import { TelemetryProps } from '../../shared/src/telemetry/telemetryService'
import { useObservable } from '../../shared/src/util/useObservable'
import { useExtensionAlertAnimation } from './nav/UserNavItem'

export interface LayoutProps
extends RouteComponentProps<{}>,
Expand Down Expand Up @@ -160,6 +161,19 @@ export const Layout: React.FunctionComponent<LayoutProps> = props => {

const breadcrumbProps = useBreadcrumbs()

// Control browser extension discoverability animation here.
// `Layout` is the lowest common ancestor of `UserNavItem` (target) and `RepoContainer` (trigger)
const { isExtensionAlertAnimating, startExtensionAlertAnimation } = useExtensionAlertAnimation()
const onExtensionAlertDismissed = useCallback(() => {
startExtensionAlertAnimation()
}, [startExtensionAlertAnimation])

// DEBUG
useEffect(() => {
// eslint-disable-next-line
;(window as any).onExtensionAlertDismissed = onExtensionAlertDismissed
}, [onExtensionAlertDismissed])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove after design review

useScrollToLocationHash(props.location)
// Remove trailing slash (which is never valid in any of our URLs).
if (props.location.pathname !== '/' && props.location.pathname.endsWith('/')) {
Expand All @@ -169,6 +183,7 @@ export const Layout: React.FunctionComponent<LayoutProps> = props => {
const context = {
...props,
...breadcrumbProps,
onExtensionAlertDismissed,
}

return (
Expand Down Expand Up @@ -201,6 +216,7 @@ export const Layout: React.FunctionComponent<LayoutProps> = props => {
: 'default'
}
hideNavLinks={false}
isExtensionAlertAnimating={isExtensionAlertAnimating}
/>
)}
{needsSiteInit && !isSiteInit && <Redirect to="/site-admin/init" />}
Expand Down
1 change: 1 addition & 0 deletions web/src/SourcegraphWebApp.scss
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ body,
@import './repo/FilePathBreadcrumbs.scss';
@import './repo/settings/RepoSettingsArea';
@import './repo/actions/InstallBrowserExtensionPopover.scss';
@import './repo/actions/InstallBrowserExtensionAlert.scss';
@import './components/LoaderInput';
@import './components/CodeSnippet';
@import './components/PageHeader';
Expand Down
40 changes: 38 additions & 2 deletions web/src/integration/blob-viewer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,11 @@ describe('Blob viewer', () => {

describe('browser extension discoverability', () => {
const HOVER_THRESHOLD = 3
const HOVER_COUNT_KEY = 'hover-count'
it(`shows a popover about the browser extension when the user has seen ${HOVER_THRESHOLD} hovers and clicks "View on [code host]" button`, async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/github.com/sourcegraph/test/-/blob/test.ts`)
await driver.page.evaluate(() => localStorage.removeItem('hover-count'))
await driver.page.reload()

await driver.page.waitForSelector('.test-go-to-code-host', { visible: true })
// Close new tab after clicking link
Expand Down Expand Up @@ -259,9 +262,42 @@ describe('Blob viewer', () => {
)
})

it.skip(`shows an alert about the browser extension when the user has seen ${HOVER_THRESHOLD} hovers`, async () => {
it(`shows an alert about the browser extension when the user has seen ${HOVER_THRESHOLD} hovers`, async () => {
await driver.page.goto(`${driver.sourcegraphBaseUrl}/github.com/sourcegraph/test/-/blob/test.ts`)
// TODO
await driver.page.evaluate(HOVER_COUNT_KEY => localStorage.removeItem(HOVER_COUNT_KEY), HOVER_COUNT_KEY)
await driver.page.reload()
Comment on lines 266 to +268
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to clear localStorage before each test? Clearing it in page.evaluateOnNewDocument would break this test, since that would run on reload. There may be a solution with request interception.

Fwiw, the test still passed every time I ran it without reloading after clearing localStorage. I just want to eliminate any possibility of flakiness.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all I can think of - would ensure 100% reproducability:

beforeEach(async () => {
  await driver.page.evaluate(() => localStorage.clear())
})


// Alert should not be visible before the user reaches the hover threshold
assert(
!(await driver.page.$('.install-browser-extension-alert')),
'Expected "Install browser extension" alert to not be displayed before user reaches hover threshold'
)

// Click 'console' and 'log' $HOVER_THRESHOLD times combined
await driver.page.waitForSelector('.test-log-token', { visible: true })
for (let index = 0; index < HOVER_THRESHOLD; index++) {
await driver.page.click(index % 2 === 0 ? '.test-log-token' : '.test-console-token')
await driver.page.waitForSelector('.hover-overlay', { visible: true })
}
await driver.page.reload()

await driver.page.waitForSelector('.repo-header')
// Alert should be visible now that the user has seen $HOVER_THRESHOLD hovers
assert(
!!(await driver.page.$('.install-browser-extension-alert')),
'Expected "Install browser extension" alert to be displayed after user reaches hover threshold'
)

// Dismiss alert
await driver.page.click('.test-close-alert')
await driver.page.reload()

await driver.page.waitForSelector('.repo-header')
// Alert should not show up now that the user has dismissed it once
assert(
!(await driver.page.$('.install-browser-extension-alert')),
'Expected "Install browser extension" alert to not be displayed before user dismisses it once'
)
})
})
})
Expand Down
1 change: 1 addition & 0 deletions web/src/nav/GlobalNavbar.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const defaultProps = (
isLightTheme: props.isLightTheme,
navbarSearchQueryState: { cursorPosition: 0, query: '' },
onNavbarQueryChange: () => {},
isExtensionAlertAnimating: false,
showCampaigns: true,
activation: undefined,
hideNavLinks: false,
Expand Down
1 change: 1 addition & 0 deletions web/src/nav/GlobalNavbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const PROPS: React.ComponentProps<typeof GlobalNavbar> = {
telemetryService: {} as any,
hideNavLinks: true, // used because reactstrap Popover is incompatible with react-test-renderer
filtersInQuery: {} as any,
isExtensionAlertAnimating: false,
splitSearchModes: false,
interactiveSearchMode: false,
toggleSearchMode: () => undefined,
Expand Down
2 changes: 2 additions & 0 deletions web/src/nav/GlobalNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { VersionContext } from '../schema/site.schema'
import { TelemetryProps } from '../../../shared/src/telemetry/telemetryService'
import { BrandLogo } from '../components/branding/BrandLogo'
import { LinkOrSpan } from '../../../shared/src/components/LinkOrSpan'
import { ExtensionAlertAnimationProps } from './UserNavItem'

interface Props
extends SettingsCascadeProps,
Expand All @@ -40,6 +41,7 @@ interface Props
TelemetryProps,
ThemeProps,
ThemePreferenceProps,
ExtensionAlertAnimationProps,
ActivationProps,
PatternTypeProps,
CaseSensitivityProps,
Expand Down
1 change: 1 addition & 0 deletions web/src/nav/NavLinks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ describe('NavLinks', () => {
authenticatedUser={authenticatedUser}
showDotComMarketing={showDotComMarketing}
location={H.createLocation(path, history.location)}
isExtensionAlertAnimating={false}
/>
</MemoryRouter>
)
Expand Down
3 changes: 2 additions & 1 deletion web/src/nav/NavLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SettingsCascadeProps } from '../../../shared/src/settings/settings'
import { WebActionsNavItems, WebCommandListPopoverButton } from '../components/shared'
import { ThemeProps } from '../../../shared/src/theme'
import { StatusMessagesNavItem } from './StatusMessagesNavItem'
import { UserNavItem } from './UserNavItem'
import { ExtensionAlertAnimationProps, UserNavItem } from './UserNavItem'
import { CampaignsNavItem } from '../enterprise/campaigns/global/nav/CampaignsNavItem'
import { ThemePreferenceProps } from '../theme'
import {
Expand All @@ -33,6 +33,7 @@ interface Props
PlatformContextProps<'forceUpdateTooltip' | 'settings' | 'sourcegraphURL'>,
ThemeProps,
ThemePreferenceProps,
ExtensionAlertAnimationProps,
TelemetryProps,
ActivationProps {
location: H.Location
Expand Down
64 changes: 64 additions & 0 deletions web/src/nav/UserNavItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,68 @@
&__dropdown-menu {
min-width: 12rem;
}

&__tooltip {
opacity: 0;
animation: tooltip-fade-in-out 5s ease-out 100ms;
}

&__avatar-background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: 50%;
opacity: 0;
animation: background-fade-in-out 5s ease-out 100ms;

background: rgba(56, 117, 127, 0.3);
filter: blur(1px);
}
}

@keyframes tooltip-fade-in-out {
0% {
opacity: 0;
}

10% {
opacity: 1;
}

84% {
opacity: 1;
}

100% {
opacity: 0;
}
}

@keyframes background-fade-in-out {
0% {
opacity: 0;
transform: scale(1, 1);
}

11% {
opacity: 0;
transform: scale(1, 1);
}

27% {
opacity: 1;
transform: scale(1.5, 1.5);
}

84% {
opacity: 1;
transform: scale(1.5, 1.5);
}

100% {
opacity: 0;
transform: scale(1, 1);
}
}
15 changes: 4 additions & 11 deletions web/src/nav/UserNavItem.story.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { action } from '@storybook/addon-actions'
import { boolean } from '@storybook/addon-knobs'
import { storiesOf } from '@storybook/react'
import React, { useCallback } from 'react'
import React from 'react'
import { ThemePreference } from '../theme'
import { UserNavItem } from './UserNavItem'
import { WebStory } from '../components/WebStory'
Expand All @@ -10,22 +10,14 @@ const onThemePreferenceChange = action('onThemePreferenceChange')

const { add } = storiesOf('web/UserNavItem', module)

const OpenUserNavItem: React.FunctionComponent<UserNavItem['props']> = props => {
const openDropdown = useCallback((userNavItem: UserNavItem | null) => {
if (userNavItem) {
userNavItem.setState({ isOpen: true })
}
}, [])
return <UserNavItem {...props} ref={openDropdown} />
}

add(
'Site admin',
() => (
<WebStory>
{webProps => (
<OpenUserNavItem
<UserNavItem
{...webProps}
testIsOpen={true}
authenticatedUser={{
username: 'alice',
avatarURL: null,
Expand Down Expand Up @@ -54,6 +46,7 @@ add(
themePreference={webProps.isLightTheme ? ThemePreference.Light : ThemePreference.Dark}
onThemePreferenceChange={onThemePreferenceChange}
showDotComMarketing={boolean('showDotComMarketing', true)}
isExtensionAlertAnimating={false}
/>
)}
</WebStory>
Expand Down
1 change: 1 addition & 0 deletions web/src/nav/UserNavItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('UserNavItem', () => {
location={history.location}
authenticatedUser={USER}
showDotComMarketing={true}
isExtensionAlertAnimating={false}
/>
</MemoryRouter>
)
Expand Down
Loading