Skip to content
Merged
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
15 changes: 15 additions & 0 deletions src/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ declare global {
debug: (message: string) => void
// FIX: Used only in puppeteer test environment. So need way to switch global context based on environment.
delay: (ms: number) => Promise<boolean>
/**
* The Navigation API, used by the Navigate Back/Forward commands. Not yet included in the TypeScript DOM lib, so a minimal subset is declared here. Optional because it is unsupported in some browsers (e.g. older Safari).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigation
*/
navigation?: {
/** True if there is a previous entry in the history that can be navigated to. */
readonly canGoBack: boolean
/** True if there is a next entry in the history that can be navigated to. */
readonly canGoForward: boolean
/** Navigates to the previous entry in the history. */
back: () => void
/** Navigates to the next entry in the history. */
forward: () => void
}
}

interface Navigator {
Expand Down
95 changes: 95 additions & 0 deletions src/commands/__tests__/navigate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import navigateBack from '../navigateBack'
import navigateForward from '../navigateForward'

/** Stubs window.navigation with the given capabilities and returns spies for back and forward. */
const stubNavigation = ({ canGoBack, canGoForward }: { canGoBack: boolean; canGoForward: boolean }) => {
const back = vi.fn()
const forward = vi.fn()
window.navigation = { canGoBack, canGoForward, back, forward }
return { back, forward }
}

/** Overrides window.history.length for the duration of a test and returns a restore function. */
const stubHistoryLength = (length: number) => {
const descriptor = Object.getOwnPropertyDescriptor(window.history, 'length')
Object.defineProperty(window.history, 'length', { configurable: true, get: () => length })
return () => {
if (descriptor) Object.defineProperty(window.history, 'length', descriptor)
}
}

afterEach(() => {
delete window.navigation
})

describe('navigateBack', () => {
it('canExecute falls back to window.history.length when the Navigation API is unavailable', () => {
delete window.navigation

const restoreEmpty = stubHistoryLength(1)
expect(navigateBack.canExecute!({} as never)).toBe(false)
restoreEmpty()

const restoreNonEmpty = stubHistoryLength(2)
expect(navigateBack.canExecute!({} as never)).toBe(true)
restoreNonEmpty()
})

it('canExecute mirrors window.navigation.canGoBack', () => {
stubNavigation({ canGoBack: true, canGoForward: false })
expect(navigateBack.canExecute!({} as never)).toBe(true)

stubNavigation({ canGoBack: false, canGoForward: true })
expect(navigateBack.canExecute!({} as never)).toBe(false)
})

it('exec navigates back in history', () => {
const { back } = stubNavigation({ canGoBack: true, canGoForward: false })
navigateBack.exec(vi.fn(), () => ({}) as never, {} as never, { type: 'keyboard' })
expect(back).toHaveBeenCalledTimes(1)
})

it('exec falls back to window.history.back when the Navigation API is unavailable', () => {
delete window.navigation
const back = vi.spyOn(window.history, 'back').mockImplementation(() => {})
navigateBack.exec(vi.fn(), () => ({}) as never, {} as never, { type: 'keyboard' })
expect(back).toHaveBeenCalledTimes(1)
back.mockRestore()
})
})

describe('navigateForward', () => {
it('canExecute falls back to window.history.length when the Navigation API is unavailable', () => {
delete window.navigation

const restoreEmpty = stubHistoryLength(1)
expect(navigateForward.canExecute!({} as never)).toBe(false)
restoreEmpty()

const restoreNonEmpty = stubHistoryLength(2)
expect(navigateForward.canExecute!({} as never)).toBe(true)
restoreNonEmpty()
})

it('canExecute mirrors window.navigation.canGoForward', () => {
stubNavigation({ canGoBack: false, canGoForward: true })
expect(navigateForward.canExecute!({} as never)).toBe(true)

stubNavigation({ canGoBack: true, canGoForward: false })
expect(navigateForward.canExecute!({} as never)).toBe(false)
})

it('exec navigates forward in history', () => {
const { forward } = stubNavigation({ canGoBack: false, canGoForward: true })
navigateForward.exec(vi.fn(), () => ({}) as never, {} as never, { type: 'keyboard' })
expect(forward).toHaveBeenCalledTimes(1)
})

it('exec falls back to window.history.forward when the Navigation API is unavailable', () => {
delete window.navigation
const forward = vi.spyOn(window.history, 'forward').mockImplementation(() => {})
navigateForward.exec(vi.fn(), () => ({}) as never, {} as never, { type: 'keyboard' })
expect(forward).toHaveBeenCalledTimes(1)
forward.mockRestore()
})
})
5 changes: 1 addition & 4 deletions src/commands/categorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ const categorizeCommand: Command = {
label: 'Categorize',
description: 'Move the current thought into a new, empty thought at the same level.',
gesture: 'lu',
keyboard: [
{ key: 'o', meta: true, alt: true },
{ key: ']', meta: true },
],
keyboard: { key: 'o', meta: true, alt: true },
// Multicursor functionality is handled in the categorize action.
// TODO: Implement this with multicursor: true so that we don't need to make a special case of this command in Select All chaining.
// See: useFilteredCommands
Expand Down
2 changes: 2 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export { default as moveCursorBackward } from './moveCursorBackward'
export { default as moveCursorForward } from './moveCursorForward'
export { default as moveThoughtDown } from './moveThoughtDown'
export { default as moveThoughtUp } from './moveThoughtUp'
export { default as navigateBack } from './navigateBack'
export { default as navigateForward } from './navigateForward'
export { default as newGrandChild } from './newGrandChild'
export { default as newSubthought } from './newSubthought'
export { default as newSubthoughtTop } from './newSubthoughtTop'
Expand Down
16 changes: 16 additions & 0 deletions src/commands/navigateBack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Command from '../@types/Command'
import ArrowLeftIcon from '../components/icons/ArrowLeftIcon'

const navigateBackCommand: Command = {
id: 'navigateBack',
label: 'Navigate Back',
description: 'Navigate to the previous page in the browser history.',
keyboard: { key: '[', meta: true },
multicursor: false,
svg: ArrowLeftIcon,
// Prefer the Navigation API's canGoBack when available; otherwise fall back to the history length as a rough approximation of whether there is anywhere to go back to.
canExecute: () => (window.navigation ? window.navigation.canGoBack : window.history.length > 1),
exec: () => (window.navigation ? window.navigation.back() : window.history.back()),
}

export default navigateBackCommand
16 changes: 16 additions & 0 deletions src/commands/navigateForward.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Command from '../@types/Command'
import ArrowRightIcon from '../components/icons/ArrowRightIcon'

const navigateForwardCommand: Command = {
id: 'navigateForward',
label: 'Navigate Forward',
description: 'Navigate to the next page in the browser history.',
keyboard: { key: ']', meta: true },
multicursor: false,
svg: ArrowRightIcon,
// Prefer the Navigation API's canGoForward when available; otherwise fall back to the history length as a rough approximation of whether there is anywhere to go forward to.
canExecute: () => (window.navigation ? window.navigation.canGoForward : window.history.length > 1),
exec: () => (window.navigation ? window.navigation.forward() : window.history.forward()),
}

export default navigateForwardCommand
5 changes: 2 additions & 3 deletions src/components/icons/ArrowLeftIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { FC } from 'react'
import { css, cx } from '../../../styled-system/css'
import { iconRecipe } from '../../../styled-system/recipes'
import { token } from '../../../styled-system/tokens'
import IconType from '../../@types/IconType'

/** Arrow-left icon used by the Command Universe Back button. */
const ArrowLeftIcon: FC<IconType> = ({ size = 24, fill, cssRaw }) => {
/** Arrow-left icon used by the Command Universe Back button and the Navigate Back command. */
const ArrowLeftIcon = ({ size = 24, fill, cssRaw }: IconType) => {
const strokeColor = fill || token('colors.fg')

return (
Expand Down
5 changes: 2 additions & 3 deletions src/components/icons/ArrowRightIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { FC } from 'react'
import { css, cx } from '../../../styled-system/css'
import { iconRecipe } from '../../../styled-system/recipes'
import { token } from '../../../styled-system/tokens'
import IconType from '../../@types/IconType'

/** Arrow-right icon used by the Command Universe Forward button. */
const ArrowRightIcon: FC<IconType> = ({ size = 24, fill, cssRaw }) => {
/** Arrow-right icon used by the Command Universe Forward button and the Navigate Forward command. */
const ArrowRightIcon = ({ size = 24, fill, cssRaw }: IconType) => {
const strokeColor = fill || token('colors.fg')

return (
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,8 @@ export const COMMAND_GROUPS: {
'jumpForward',
'moveCursorBackward',
'moveCursorForward',
'navigateBack',
'navigateForward',
'openDesktopCommandUniverse',
'home',
'search',
Expand Down
10 changes: 5 additions & 5 deletions src/e2e/puppeteer/__tests__/categorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,23 @@ describe('categorize', () => {
await clickThought(topParagraphText)

// Perform 1st categorization
await press(']', { meta: true })
await press('o', { meta: true, alt: true })
await press('1')

// Perform 2nd categorization
await press(']', { meta: true })
await press('o', { meta: true, alt: true })
await press('2')

// Perform 3rd categorization
await press(']', { meta: true })
await press('o', { meta: true, alt: true })
await press('3')

// Perform 4th categorization
await press(']', { meta: true })
await press('o', { meta: true, alt: true })
await press('4')

// Perform 5th categorization
await press(']', { meta: true })
await press('o', { meta: true, alt: true })
await press('5')

const imageCategorized = await screenshot()
Expand Down
Loading