Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests for Tutorial #2702

Merged
merged 22 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from 17 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
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>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it was a bug I've found while testing

) : (
<>
<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}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's duplicated by fastClick, which caused a skip of step I've painfully experienced multiple times while testing

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, good catch. It doesn't make sense to have both.

{...{ disabled }}
{...fastClick(clickHandler)}
ref={ref}
Expand Down
296 changes: 296 additions & 0 deletions src/components/Tutorial/__tests__/Tutorial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import { getByText, screen } from '@testing-library/dom'
import userEvent from '@testing-library/user-event'
import { act } from 'react'
import {
HOME_TOKEN,
TUTORIAL2_STEP_CHOOSE,
TUTORIAL2_STEP_CONTEXT1,
TUTORIAL2_STEP_CONTEXT1_PARENT,
TUTORIAL2_STEP_CONTEXT1_SUBTHOUGHT,
TUTORIAL2_STEP_CONTEXT2_PARENT,
TUTORIAL2_STEP_CONTEXT_VIEW_EXAMPLES,
TUTORIAL2_STEP_CONTEXT_VIEW_OPEN,
TUTORIAL2_STEP_CONTEXT_VIEW_SELECT,
TUTORIAL2_STEP_START,
TUTORIAL_STEP_AUTOEXPAND,
TUTORIAL_STEP_AUTOEXPAND_EXPAND,
TUTORIAL_STEP_FIRSTTHOUGHT,
TUTORIAL_STEP_FIRSTTHOUGHT_ENTER,
TUTORIAL_STEP_SECONDTHOUGHT,
TUTORIAL_STEP_SECONDTHOUGHT_ENTER,
TUTORIAL_STEP_START,
TUTORIAL_STEP_SUBTHOUGHT,
TUTORIAL_STEP_SUBTHOUGHT_ENTER,
TUTORIAL_STEP_SUCCESS,
} from '../../../constants'
import exportContext from '../../../selectors/exportContext'
import store from '../../../stores/app'
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 })

/** Get last created thought in the document, used mostly after `user.keyboard('{Enter}')`. */
const lastThought = () => Array.from(document.querySelectorAll('[contenteditable="true"]')).at(-1)!
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use [aria-label="thought"] [contenteditable] or [data-editable="true"] to be a bit more specific. There happen to be no other contenteditable's on the screen, but it's a bit safer to not assume that.

Even better would be to specifically select the empty thought.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Even better would be to specifically select the empty thought.

I considered this during my previous review, but didn't mention it since the last thought and the empty thought were coincidentally always the same.

It would be nicer to explicitly select the thought to be edited (the empty thought), and easier to update in the future if we need to edit another thought during testing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So, I arrived at

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

I think it works. Perhaps, a more refined solution could be out there, but it works :D
I'd prefer to modify this thing and move out to test-helpers at some point if we hit the wall, but not now.


/** Ensure we are at the given step of the tutorial. */
const ensureStep = (step: number) => {
expect(store.getState().storageCache?.tutorialStep).toBe(step)
raineorshine marked this conversation as resolved.
Show resolved Hide resolved
}

describe('Tutorial 1', async () => {
beforeEach(() => createTestApp({ tutorial: true }))
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I'm missing something, but if the tests are all run sequentially on a single app instance, wouldn't this need to be beforeAll instead of beforeEach?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I really have not much idea how to make it work with one beforeAll, since if I don't create a new test app for each case, I'm left with empty-ish DOM
I think, the whole setup relies on re-creating an isolated testing env for each case, as it's done on every other test suite.

right now it's run with one db/store/state/whatever, but with cleaning up event listeners that keep attaching
but it's not one sequential run per se, though I'd argue it's a lot like reloading page at each step :D

I'm really not sure how to make it work with one beforeAll, really

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for your reply. I'd like to dig a little deeper into this. The tests are quite readable right now, but it's not clear what state they share between test blocks.

I really have not much idea how to make it work with one beforeAll, since if I don't create a new test app for each case, I'm left with empty-ish DOM

What do you mean by "empty-ish DOM"?

I think, the whole setup relies on re-creating an isolated testing env for each case, as it's done on every other test suite.

Are you saying that the current implementation recreates an isolated testing environment for each case? Because my understanding is that these tests depend on the current test step being preserved between tests.

right now it's run with one db/store/state/whatever, but with cleaning up event listeners that keep attaching

The event listeners only keep attaching because you remove them and re-attach them each time.

but it's not one sequential run per se, though I'd argue it's a lot like reloading page at each step :D

Is it a sequential run or not? Or some hybrid? I think for clarity we need to stick with one or the other, or more carefully document the unique combination of shared state between test runs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What do you mean by "empty-ish DOM"?

html
<body
  data-browser="safari"
  data-color-mode="dark"
  data-device="desktop"
  data-drag-in-progress="false"
  data-native="false"
  data-platform="other"
/>

Are you saying that the current implementation recreates an isolated testing environment for each case? Because my understanding is that these tests depend on the current test step being preserved between tests.

we have everything in db, store etc., but dom is empty if we don't call the createTestApp on each test case
so the current approach, and me attaching and detaching events, is:

  1. we createTestApp, get correct dom state, built from store/db.
  2. we run tests
  3. we remove event listeners created

Just runing a createTestApp once does not work, and it's a problem of our general testing environment.
It's easy to demonstrate if we try to do this:

  1. we createTestApp
  2. we run one test, where we create a thought "first"
  3. we should have "first" thought present in our dom

but it does not work like this at all.
the test suite below gets 2/2 green, but it should not.

describe.only('T_T', () => {
  beforeAll(createTestApp)

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

    expect(exportContext(store.getState(), HOME_TOKEN)).toEqual(`<ul>
  <li>__ROOT__  
    <ul>
      <li>first</li>
      <li></li>
    </ul>
  </li>
</ul>`)
    expect(screen.getByText('first')).toBeInTheDocument()
  })

  it('we expect to see "first" thought', () => {
    console.log(exportContext(store.getState(), HOME_TOKEN))

    screen.debug(document.querySelector('.content')!) /* <-- prints this
    <body
      data-browser="safari"
      data-color-mode="dark"
      data-device="desktop"
      data-drag-in-progress="false"
      data-native="false"
      data-platform="other"
    />
    */

    expect(() => screen.getByText('first')).toThrow()
  })
})

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

maybe we should "fix" this, but will it break other tests?

i generally feel like this pr now achieves what we want it to, given the current testing environment
maybe we should create another issue for it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks. I'd love to figure out why the DOM is empty. Without knowing the answer to that, it's hard to know what the right call is here.

However, since all the tests pass and they are quite readable, I'm okay with merging the current implementation so that we have a good checkpoint in the commit history. We can investigate further in a separate issue.

If you could address the small stylistic changes below, then I'd be happy to merge. Thanks!

afterEach(cleanupTestEventHandlers)
afterAll(cleanupTestApp)
describe('step start', () => {
it('shows the welcome text', () => {
ensureStep(TUTORIAL_STEP_START)
expect(screen.getByText('Welcome to your personal thoughtspace.')).toBeInTheDocument()
})

it('shows the "Next" button, that leads to the first step', async () => {
await user.click(screen.getByText('Next'))
expect(screen.getByText(/First, let me show you how to create a new thought/)).toBeInTheDocument()
})
})

it('step first thought - how to create a thought ', async () => {
ensureStep(TUTORIAL_STEP_FIRSTTHOUGHT)
expect(screen.getByText('Hit the Enter key to create a new thought.')).toBeInTheDocument()
await user.keyboard('{Enter}')
expect(screen.getByText('You did it!')).toBeInTheDocument()
Copy link
Contributor

Choose a reason for hiding this comment

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

Test actions look good and are very readable 👍


ensureStep(TUTORIAL_STEP_FIRSTTHOUGHT_ENTER)
expect(screen.getByText('Now type something. Anything will do.')).toBeInTheDocument()
await user.type(lastThought(), 'my first thought')
await user.keyboard('{Enter}')
await act(vi.runOnlyPendingTimersAsync)
})

it('step second thought - prompts to add another thought', async () => {
expect(screen.getByText('Well done!')).toBeInTheDocument()
expect(screen.getByText(/Try adding another thought/)).toBeInTheDocument()
ensureStep(TUTORIAL_STEP_SECONDTHOUGHT)
Copy link
Contributor

Choose a reason for hiding this comment

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

Actions should generally be performed in the test that depends on them. So instead of typing and hitting enter in one test and asserting the result in the next, move the typing and hitting enter to the next test where it is asserted.

As a general rule, a test should end with an expect. Anything after that should probably belong to the next test.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with this.

Some testing philosophies go further and recommend/require exactly one expect statement at the end of each test, but I believe that might be an excessively narrow rule for these tests.


await user.type(lastThought(), 'my second thought')
await user.keyboard('{Enter}')
await act(vi.runOnlyPendingTimersAsync)

ensureStep(TUTORIAL_STEP_SECONDTHOUGHT_ENTER)

expect(screen.getByText('Good work!')).toBeInTheDocument()
expect(screen.getByText('Now type some text for the new thought.')).toBeInTheDocument()
await user.type(lastThought(), 'third thought')
await user.keyboard('{Enter}')
await act(vi.runOnlyPendingTimersAsync)
Copy link
Contributor

Choose a reason for hiding this comment

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

There are still a few places where tests do not end in expect.

See: #2702 (comment)

})

it('step subthought - how to create a thought inside thought', async () => {
ensureStep(TUTORIAL_STEP_SUBTHOUGHT)
expect(exportContext(store.getState(), [HOME_TOKEN], 'text/plain')).toBe(
`- __ROOT__
- my first thought
- my second thought
- third thought
- `,
)
raineorshine marked this conversation as resolved.
Show resolved Hide resolved

// Now I am going to show you how to add a thought within another thought.
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment is redundant and can safely be removed.

Likewise with similar redundant comments below.

expect(screen.getByText(/Now I am going to show you how to add a thought/)).toBeInTheDocument()

// since we have cursor on empty thought
expect(screen.getByText(/Hit the Delete key to delete the current blank thought/)).toBeInTheDocument()
expect(screen.getByText(/Then hold the Ctrl key and hit the Enter key/)).toBeInTheDocument()
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's stick with a single screen.getByText for a given point in the tutorial. Testing the minimal unique text at the given step will give us effectively the same test coverage, but will be less maintenance if the tutorial text is changed in the future.

Likewise in similar occurrences below.

Copy link
Contributor

Choose a reason for hiding this comment

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

@snqb Still needs attention

await user.keyboard('{Backspace}')
await user.keyboard('{Control>}{Enter}{/Control}')
await act(vi.runOnlyPendingTimersAsync)

await user.type(lastThought(), 'child')
await act(vi.runOnlyPendingTimersAsync)
expect(exportContext(store.getState(), [HOME_TOKEN], 'text/plain')).toBe(
`- __ROOT__
- my first thought
- my second thought
- third thought
- child`,
)
// as you can see, the new thought "child" is nested within "third thought"
expect(screen.getByText(/As you can see, the new thought "child" is nested/)).toBeInTheDocument()
expect(getByText(screen.getByTestId('tutorial-step'), /"third thought"/)).toBeInTheDocument()

ensureStep(TUTORIAL_STEP_SUBTHOUGHT_ENTER)
await user.click(screen.getByText('Next'))
})

describe('step autoexpand', async () => {
it('thoughts are automatically hidden when you click away', async () => {
ensureStep(TUTORIAL_STEP_AUTOEXPAND)
expect(screen.getByText(/thoughts are automatically hidden when you click away/)).toBeInTheDocument()
})

it('click on "uncle" thought to hide child', async () => {
await user.click(screen.getByText('my second thought'))
expect(screen.getByText('Notice that "child" is hidden now.', { exact: false })).toBeInTheDocument()
ensureStep(TUTORIAL_STEP_AUTOEXPAND_EXPAND)
})

it('click back on a "parent" thought to reveal child', async () => {
expect(screen.getByText(/Click "third thought" to reveal its subthought "child"/)).toBeInTheDocument()
await user.click(screen.getAllByText('third thought').at(-1)!)
expect(screen.getByText('Lovely. You have completed the tutorial', { exact: false })).toBeInTheDocument()
ensureStep(TUTORIAL_STEP_SUCCESS)
})
})

describe('step success - congratulates on completing first tutorial', async () => {
it('congratulate on completing first tutorial', async () => {
expect(screen.getByText('Lovely. You have completed the tutorial', { exact: false })).toBeInTheDocument()
})

it('asks to continue with tutorial or play on own', async () => {
expect(screen.getByText('Play on my own')).toBeInTheDocument()
expect(screen.getByText('Learn more')).toBeInTheDocument()
})
})
})

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

ensureStep(TUTORIAL2_STEP_START)
expect(screen.getByText(`If the same thought appears in more than one place`, { exact: false })).toBeInTheDocument()
expect(
screen.getByText(`shows a small number to the right of the thought, for example`, { exact: false }),
).toBeInTheDocument()
})

it('step choose - gives 3 choices of what project to proceed with', async () => {
await user.click(screen.getByText('Next'))
ensureStep(TUTORIAL2_STEP_CHOOSE)
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('step context 1 parent - we create "Home" thought with children', 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()
ensureStep(TUTORIAL2_STEP_CONTEXT1_PARENT)

// we create a `Home` thought
await user.keyboard('{Enter}')
await user.type(lastThought(), 'Home')
await act(vi.runOnlyPendingTimersAsync)
ensureStep(TUTORIAL2_STEP_CONTEXT1)

// Home -> To Do
expect(screen.getByText(/Let's say that you want to make a list of things/)).toBeInTheDocument()
expect(screen.getByText(/Add a thought with the text "To Do"/)).toBeInTheDocument()
await user.keyboard('{Control>}{Enter}{/Control}')
await user.type(lastThought(), 'To Do')
await act(vi.runOnlyPendingTimersAsync)
ensureStep(TUTORIAL2_STEP_CONTEXT1_SUBTHOUGHT)

// Home -> To Do -> or to not
expect(screen.getByText(/Now add a thought to “To Do”/)).toBeInTheDocument()
await user.keyboard('{Control>}{Enter}{/Control}')
await user.type(lastThought(), 'or to not')
await act(vi.runOnlyPendingTimersAsync)

expect(exportContext(store.getState(), [HOME_TOKEN], 'text/plain')).toBe(
`- __ROOT__
- Home
- To Do
- or to not`,
)
expect(screen.getByText(/Nice work!/)).toBeInTheDocument()
})

it('step context 2 - we create "Work" thought with children', async () => {
await user.click(screen.getByText('Next'))
ensureStep(TUTORIAL2_STEP_CONTEXT2_PARENT)
expect(screen.getByText(/Now we are going to create a different "To Do" list./)).toBeInTheDocument()

// we created a new thought on 3rd level, so we shift-tab our way back to root
await user.keyboard('{Enter}')
await user.keyboard('{Shift>}{Tab}{/Shift}')
await user.keyboard('{Shift>}{Tab}{/Shift}')
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't mind this, but it might be more natural to select a thought at the root and create the new thought from there with one enter (and not use shift+tab).

Thoughts, @raineorshine?

Copy link
Contributor

@raineorshine raineorshine Jan 17, 2025

Choose a reason for hiding this comment

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

Yes, I agree. Shift + Tab activates the Outdent command. It would be better to stick to simple navigation and New Thought commands for completing the tutorial. That way we can avoid coupling the tests to an unrelated command.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

totally makes sense, thanks
now it's

await user.click(screen.getByText('Home'))
await user.keyboard('{Enter}')

await user.type(lastThought(), 'Work')
await act(vi.runOnlyPendingTimersAsync)

expect(screen.getByText('Now add a thought with the text "To Do"', { exact: false })).toBeInTheDocument()
await user.keyboard('{Control>}{Enter}{/Control}')
await user.type(lastThought(), 'To Do')

expect(exportContext(store.getState(), [HOME_TOKEN], 'text/plain')).toBe(
`- __ROOT__
- Home
- To Do
- or to not
- Work
- To Do`,
)
expect(screen.getByText('Very good!')).toBeInTheDocument()

expect(screen.getAllByRole('superscript')[0]).toHaveTextContent('2')
raineorshine marked this conversation as resolved.
Show resolved Hide resolved
expect(screen.getByText('This means that “To Do” appears in two places', { exact: false })).toBeInTheDocument()

expect(screen.getByText('Imagine a new work task. Add it to this “To Do” list.'))
await user.keyboard('{Control>}{Enter}{/Control}')
await user.type(lastThought(), 'new work task')
})

it('step context view open - we press Alt+Shift+S and see "To Do" in both "Home" and "Work"', async () => {
expect(exportContext(store.getState(), [HOME_TOKEN], 'text/plain')).toBe(
`- __ROOT__
- Home
- To Do
- or to not
- Work
- To Do
- new work task`,
)
await user.click(screen.getByText('Next'))
await act(vi.runOnlyPendingTimersAsync)

ensureStep(TUTORIAL2_STEP_CONTEXT_VIEW_SELECT)
expect(screen.getByText(/First select "To Do"./)).toBeInTheDocument()
await user.keyboard('{Escape}') // focus out
await user.click(screen.getAllByText('To Do').at(-1)!) // and click on To Do
await act(vi.runOnlyPendingTimersAsync)

expect(screen.getByText("Hit Alt + Shift + S to view the current thought's contexts.")).toBeInTheDocument()
await user.keyboard('{Alt>}{Shift>}S{/Shift}{/Alt}')
await act(vi.runOnlyPendingTimersAsync)
ensureStep(TUTORIAL2_STEP_CONTEXT_VIEW_OPEN)
expect(
screen.getByText(
`Well, look at that. We now see all of the contexts in which "To Do" appears, namely "Home" and "Work".`,
),
).toBeInTheDocument()
})

it('step context examples - we see real-world examples', async () => {
await user.click(screen.getByText('Next'))
ensureStep(TUTORIAL2_STEP_CONTEXT_VIEW_EXAMPLES)
expect(screen.getByText(/Here are some real-world examples of using contexts in/)).toBeInTheDocument()
expect(screen.getByText('View all thoughts related to a particular person, place, or thing.')).toBeInTheDocument()
expect(screen.getByText('Keep track of quotations from different sources.')).toBeInTheDocument()
})

it('step success - congratulations, hide tutorial after clicking "Finish"', async () => {
await user.click(screen.getByText('Next'))
expect(screen.getByText(/Congratulations! You have completed Part II of the tutorial./)).toBeInTheDocument()

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 } = {}) => {
trevinhofmann marked this conversation as resolved.
Show resolved Hide resolved
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 },
Copy link
Collaborator

@trevinhofmann trevinhofmann Jan 17, 2025

Choose a reason for hiding this comment

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

Instead of using !!tutorial to handle the undefined cases, it would be nicer to set a default value above so that the tutorial value is always a defined boolean.

Copy link
Contributor

Choose a reason for hiding this comment

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

FYI This is a small concession to allow point-free style in beforeEach.

See: #2702 (comment)


// 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 () => {
raineorshine marked this conversation as resolved.
Show resolved Hide resolved
await act(async () => {
if (cleanup) {
cleanup()
}
})
}

export default createTestApp
Loading