Skip to content

Commit

Permalink
Tests for Tutorial (#2702)
Browse files Browse the repository at this point in the history
  • Loading branch information
snqb authored Jan 19, 2025
1 parent 8a626f5 commit 8126692
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/components/Tutorial/Tutorial2StepContextViewOpen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const Tutorial2StepContextViewOpen = () => {
{TUTORIAL_CONTEXT1_PARENT[tutorialChoice]}" or "{TUTORIAL_CONTEXT2_PARENT[tutorialChoice]}" to show it again.
</p>
) : contextViewClosed ? (
<p>Oops, somehow the context view was closed. Select "Relationships".</p>
<p>Oops, somehow the context view was closed. Select "{TUTORIAL_CONTEXT[tutorialChoice]}".</p>
) : (
<>
<p>
Expand Down
1 change: 0 additions & 1 deletion src/components/Tutorial/TutorialNavigationButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const TutorialNavigationButton = React.forwardRef<
>(({ clickHandler, value, disabled = false, classes }, ref) => (
<a
className={cx(anchorButtonRecipe({ variableWidth: true, smallGapX: true }), classes)}
onClick={clickHandler}
{...{ disabled }}
{...fastClick(clickHandler)}
ref={ref}
Expand Down
260 changes: 260 additions & 0 deletions src/components/Tutorial/__tests__/Tutorial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { screen } from '@testing-library/dom'
import userEvent from '@testing-library/user-event'
import { act } from 'react'
import createTestApp, { cleanupTestApp, cleanupTestEventHandlers } from '../../../test-helpers/createTestApp'

// as per https://testing-library.com/docs/user-event/options/#advancetimers
// we should avoid using { delay: null }, and use jest.advanceTimersByTime instead
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime })

/** Gets the last empty thought in the document. Mostly used after `user.keyboard('{Enter}')` to get the new thought. */
const lastEmptyThought = () =>
Array.from(document.querySelectorAll('[data-editable="true"]'))
.filter(it => !it.textContent)
.at(-1)!

describe('Tutorial 1', async () => {
beforeEach(() => createTestApp({ tutorial: true }))
afterEach(cleanupTestEventHandlers)
afterAll(cleanupTestApp)
describe('step start', () => {
it('we see the welcome text', () => {
expect(screen.getByText('Welcome to your personal thoughtspace.')).toBeInTheDocument()
})

it('we can proceed to first step by clicking Next', async () => {
await user.click(screen.getByText('Next'))
expect(screen.getByText('Hit the Enter key to create a new thought.')).toBeInTheDocument()
})
})

describe('step first thought', () => {
it('we learn how to create a thought', async () => {
await user.keyboard('{Enter}')
expect(screen.getByText('Now type something. Anything will do.')).toBeInTheDocument()
})

it('we create our first thought', async () => {
await user.type(lastEmptyThought(), 'first')
await user.keyboard('{Enter}')
await act(vi.runOnlyPendingTimersAsync)

expect(screen.getByText('Well done!')).toBeInTheDocument()
})
})

it('we create a second thought', async () => {
expect(screen.getByText(/Try adding another thought/)).toBeInTheDocument()
await user.type(lastEmptyThought(), 'second')
await user.keyboard('{Enter}')
await act(vi.runOnlyPendingTimersAsync)

expect(screen.getByText('Now type some text for the new thought.')).toBeInTheDocument()
})

it('we create a third thought', async () => {
await user.type(lastEmptyThought(), 'third')
await user.keyboard('{Enter}')
await act(vi.runOnlyPendingTimersAsync)

expect(screen.getByText(/Hit the Delete key to delete the current blank thought/)).toBeInTheDocument()
})

it('we learn to create nested thoughts', async () => {
/* thoughts:
- first
- second
- third
-
*/
await user.keyboard('{Backspace}')
await user.keyboard('{Control>}{Enter}{/Control}')
await act(vi.runOnlyPendingTimersAsync)

await user.type(lastEmptyThought(), 'child of third')
await act(vi.runOnlyPendingTimersAsync)

expect(screen.getByText(/As you can see, the new thought "child of third" is nested/)).toBeInTheDocument()
})

describe('step autoexpand', async () => {
it('we learn about automatic thought hiding', async () => {
await user.click(screen.getByText('Next'))
expect(screen.getByText(/thoughts are automatically hidden when you click away/)).toBeInTheDocument()
})

it('we hide nested thoughts by clicking away', async () => {
/* thoughts:
- first
- second
- third
- child of third
*/
await user.click(screen.getByText('second'))
expect(screen.getByText(/Notice that "child of third" is hidden now/)).toBeInTheDocument()
})

it('we reveal hidden thoughts by clicking parent', async () => {
await user.click(screen.getAllByText('third').at(-1)!)
expect(screen.getByText('child of third')).toBeInTheDocument()
})
})

describe('step success', async () => {
it('we complete the first tutorial', async () => {
expect(screen.getByText(/Lovely\. You have completed the tutorial/)).toBeInTheDocument()
})

it('we can exit tutorial mode', async () => {
await user.click(screen.getByText('Play on my own'))
await act(vi.runOnlyPendingTimersAsync)
expect(() => screen.getByTestId('tutorial-step')).toThrow('Unable to find an element')
})

it('we can continue to advanced tutorial', async () => {
expect(screen.getByText('Learn more')).toBeInTheDocument()
})
})
})

describe('Tutorial 2', async () => {
beforeEach(() => createTestApp({ tutorial: true }))
afterEach(cleanupTestEventHandlers)
afterAll(cleanupTestApp)
it('we learn about context view', async () => {
await user.click(screen.getByText('Learn more'))

expect(screen.getByText(/shows a small number to the right of the thought, for example/)).toBeInTheDocument()
})

it('we choose a project type, one of three options', async () => {
await user.click(screen.getByText('Next'))
expect(screen.getByText(/For this tutorial, choose what kind of content you want to create/)).toBeInTheDocument()
expect(screen.getByText('To-Do List')).toBeInTheDocument()
expect(screen.getByText('Journal Theme')).toBeInTheDocument()
expect(screen.getByText('Book/Podcast Notes')).toBeInTheDocument()
})

it('we start creating a to-do list', async () => {
await user.click(screen.getAllByText('To-Do List').at(-1)!)
expect(screen.getByText(/Excellent choice\. Now create a new thought with the text Home/)).toBeInTheDocument()
})

describe('step context 1 - create a "Home" to-do list', () => {
it('we create a "Home" thought', async () => {
await user.keyboard('{Enter}')
await user.type(lastEmptyThought(), 'Home')
await act(vi.runOnlyPendingTimersAsync)
expect(screen.getByText(/Add a thought with the text "To Do"/)).toBeInTheDocument()
})

it('we add a "Home" • "To Do" subthought', async () => {
await user.keyboard('{Control>}{Enter}{/Control}')
await user.type(lastEmptyThought(), 'To Do')
await act(vi.runOnlyPendingTimersAsync)
expect(screen.getByText(/Now add a thought to To Do/)).toBeInTheDocument()
})

it('we add a "Home" • "To Do" • "Or to not subthought" sub-subthought', async () => {
await user.keyboard('{Control>}{Enter}{/Control}')
await user.type(lastEmptyThought(), 'Or to not')
await act(vi.runOnlyPendingTimersAsync)

/* thoughts:
- Home
- To Do
- Or to not
*/
expect(screen.getByText(/Nice work!/)).toBeInTheDocument()
})
})

describe('step context 2 - create a "Work" to-do list', () => {
it('we prepare to create another list', async () => {
await user.click(screen.getByText('Next'))
expect(screen.getByText(/Now we are going to create a different "To Do" list./)).toBeInTheDocument()
})

it('we create a Work thought', async () => {
// we created a new thought on 3rd level, clicking "Home" gets us to root
await user.click(screen.getByText('Home'))
await user.keyboard('{Enter}')
await user.type(lastEmptyThought(), 'Work')
await act(vi.runOnlyPendingTimersAsync)

/* thoughts:
- Home
- To Do
- or to not
- Work
*/
expect(screen.getByText(/Now add a thought with the text "To Do"/)).toBeInTheDocument()
})

it('we add another "Work" • "To Do" thought', async () => {
await user.keyboard('{Control>}{Enter}{/Control}')
await user.type(lastEmptyThought(), 'To Do')
expect(screen.getByText('Imagine a new work task. Add it to this “To Do” list.'))
})

it('we see contexts with a superscript', async () => {
expect(screen.getAllByRole('superscript')[0]).toHaveTextContent('2')
expect(screen.getByText(/This means that To Do appears in two places/)).toBeInTheDocument()
})

it('we add a "Work" • "To Do" • "Text boss" thought', async () => {
await user.keyboard('{Control>}{Enter}{/Control}')
await user.type(lastEmptyThought(), 'Text boss')
await act(vi.runOnlyPendingTimersAsync)
/* thoughts:
- Home
- To Do
- or to not
- Work
- To Do
- Text boss
*/
expect(screen.getByText('Next')).toBeInTheDocument()
})
})

describe('step context view open', () => {
it('we learn about multiple contexts', async () => {
await user.click(screen.getByText('Next'))
await act(vi.runOnlyPendingTimersAsync)
expect(screen.getByText(/First select "To Do"./)).toBeInTheDocument()
})

it('we select a thought with multiple contexts', async () => {
await user.click(screen.getAllByText('To Do').at(-1)!)
await act(vi.runOnlyPendingTimersAsync)
expect(screen.getByText("Hit Alt + Shift + S to view the current thought's contexts.")).toBeInTheDocument()
})

it('we view contexts of a thought', async () => {
await user.keyboard('{Alt>}{Shift>}S{/Shift}{/Alt}')
await act(vi.runOnlyPendingTimersAsync)
expect(
screen.getByText(/We now see all of the contexts in which "To Do" appears, namely "Home" and "Work"./),
).toBeInTheDocument()
})
})

it('we see real-world context examples', async () => {
await user.click(screen.getByText('Next'))
expect(screen.getByText(/Here are some real-world examples of using contexts in/)).toBeInTheDocument()
})

describe('step success', () => {
it('we complete the advanced tutorial', async () => {
await user.click(screen.getByText('Next'))
expect(screen.getByText(/Congratulations! You have completed Part II of the tutorial./)).toBeInTheDocument()
})

it('we exit the tutorial', async () => {
user.click(screen.getByText('Finish'))
await act(vi.runOnlyPendingTimersAsync)
expect(() => screen.getByTestId('tutorial-step')).toThrow('Unable to find an element')
})
})
})
2 changes: 1 addition & 1 deletion src/components/Tutorial/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const Tutorial: FC = () => {
>
✕ close tutorial
</a>
<div className={css({ clear: 'both' })}>
<div className={css({ clear: 'both' })} data-testid='tutorial-step'>
<div>
<TransitionGroup>
{tutorialStepComponent ? (
Expand Down
16 changes: 12 additions & 4 deletions src/test-helpers/createTestApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import storage from '../util/storage'
let cleanup: Await<ReturnType<typeof initialize>>['cleanup']

/** Set up testing and mock document and window functions. */
const createTestApp = async () => {
const createTestApp = async ({ tutorial }: { tutorial?: boolean } = {}) => {
await act(async () => {
vi.useFakeTimers({ loopLimit: 100000 })

// calls initEvents, which must be manually cleaned up
const init = await initialize()
cleanup = init.cleanup
Expand All @@ -33,8 +32,8 @@ const createTestApp = async () => {
)

store.dispatch([
// skip tutorial
{ type: 'tutorial', value: false },
// there are cases where we want to show tutorial on test runs, whilst mostly we don't
{ type: 'tutorial', value: !!tutorial },

// close welcome modal
{ type: 'closeModal' },
Expand Down Expand Up @@ -80,4 +79,13 @@ export const refreshTestApp = async () => {
await act(vi.runOnlyPendingTimersAsync)
}

/** Clear existing event listeners(e.g. keyboard, gestures), but without clearing the app. */
export const cleanupTestEventHandlers = async () => {
await act(async () => {
if (cleanup) {
cleanup()
}
})
}

export default createTestApp

0 comments on commit 8126692

Please sign in to comment.