Skip to content

Commit

Permalink
Reset expanded directories list on category change (#11725)
Browse files Browse the repository at this point in the history
Partially closes: cloud-v2/1592

Closes: enso-org/cloud-v2#1606

This PR also adds needed configuration for unit tests and adjust it to run using vscode vite extension
  • Loading branch information
MrFlashAccount authored Dec 2, 2024
1 parent 2894618 commit 1676545
Show file tree
Hide file tree
Showing 19 changed files with 324 additions and 42 deletions.
2 changes: 1 addition & 1 deletion app/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"scripts": {
"test": "vitest run",
"lint": "eslint . --cache --max-warnings=0"
"lint": "eslint ./src --cache --max-warnings=0"
},
"peerDependencies": {
"@tanstack/query-core": "5.54.1",
Expand Down
5 changes: 5 additions & 0 deletions app/gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"@fast-check/vitest": "^0.0.8",
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@playwright/test": "^1.40.0",
"@babel/plugin-syntax-import-attributes": "^7.24.7",
"@react-types/shared": "^3.22.1",
"@storybook/addon-essentials": "^8.4.2",
"@storybook/addon-interactions": "^8.4.2",
Expand Down Expand Up @@ -179,6 +180,10 @@
"@vitest/coverage-v8": "^1.3.1",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.0.1",
"@testing-library/user-event": "14.5.2",
"@testing-library/react-hooks": "8.0.1",
"css.escape": "^1.5.1",
"d3": "^7.4.0",
"enso-common": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ export const BUTTON_STYLES = tv({
{ size: 'medium', iconOnly: true, class: { base: 'p-0 rounded-full', icon: 'w-4 h-4' } },
{ size: 'large', iconOnly: true, class: { base: 'p-0 rounded-full', icon: 'w-4.5 h-4.5' } },
{ size: 'hero', iconOnly: true, class: { base: 'p-0 rounded-full', icon: 'w-12 h-12' } },
{ fullWidth: false, class: { icon: 'flex-none' } },

{ variant: 'icon', class: { base: 'flex-none' } },

{ variant: 'link', isFocused: true, class: 'focus-visible:outline-offset-1' },
{ variant: 'link', size: 'xxsmall', class: 'font-medium' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import * as copyHook from '#/hooks/copyHooks'

import * as textProvider from '#/providers/TextProvider'

import * as ariaComponents from '#/components/AriaComponents'

import * as twv from '#/utilities/tailwindVariants'
import { Button } from '../Button'
import { TEXT_STYLE } from '../Text'

// =================
// === Constants ===
// =================

const COPY_BLOCK_STYLES = twv.tv({
base: ariaComponents.TEXT_STYLE({
base: TEXT_STYLE({
class: 'max-w-full bg-primary/5 border-primary/10',
}),
variants: {
Expand Down Expand Up @@ -58,14 +58,14 @@ export function CopyBlock(props: CopyBlockProps) {
const { copyTextBlock, base } = COPY_BLOCK_STYLES()

return (
<ariaComponents.Button
<Button
variant="custom"
size="custom"
onPress={() => mutateAsync()}
tooltip={isSuccess ? getText('copied') : getText('copy')}
className={base({ className })}
>
<span className={copyTextBlock()}>{copyText}</span>
</ariaComponents.Button>
</Button>
)
}
2 changes: 1 addition & 1 deletion app/gui/src/dashboard/components/dashboard/ProjectIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export default function ProjectIcon(props: ProjectIconProps) {
icon={StopIcon}
aria-label={userOpeningProjectTooltip ?? getText('stopExecution')}
tooltipPlacement="left"
className={tailwindMerge.twMerge(isRunningInBackground && 'text-green')}
className={tailwindMerge.twJoin(isRunningInBackground && 'text-green')}
onPress={doCloseProject}
/>
<Spinner
Expand Down
13 changes: 8 additions & 5 deletions app/gui/src/dashboard/layouts/AssetsTable/assetTreeHooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMemo } from 'react'

import { useQueries, useQuery, useQueryClient } from '@tanstack/react-query'

import type { DirectoryId } from 'enso-common/src/services/Backend'
import {
assetIsDirectory,
createRootDirectoryAsset,
Expand All @@ -11,7 +12,6 @@ import {
createSpecialLoadingAsset,
type AnyAsset,
type DirectoryAsset,
type DirectoryId,
} from 'enso-common/src/services/Backend'

import { listDirectoryQueryOptions } from '#/hooks/backendHooks'
Expand Down Expand Up @@ -88,10 +88,13 @@ export function useAssetTree(options: UseAssetTreeOptions) {
useMemo(
() => ({
queryKey: [backend.type, 'refetchListDirectory'],
queryFn: async () => {
await queryClient.refetchQueries({ queryKey: [backend.type, 'listDirectory'] })
return null
},
queryFn: () =>
queryClient
.refetchQueries({
queryKey: [backend.type, 'listDirectory'],
type: 'active',
})
.then(() => null),
refetchInterval:
enableAssetsTableBackgroundRefresh ? assetsTableBackgroundRefreshInterval : false,
refetchOnMount: 'always',
Expand Down
1 change: 1 addition & 0 deletions app/gui/src/dashboard/providers/DriveProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default function DriveProvider(props: ProjectsProviderProps) {
targetDirectory: null,
selectedKeys: EMPTY_SET,
visuallySelectedKeys: null,
expandedDirectoryIds: EMPTY_ARRAY,
})
}
},
Expand Down
38 changes: 38 additions & 0 deletions app/gui/src/dashboard/providers/__test__/DriveProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Category } from '#/layouts/CategorySwitcher/Category'
import { act, renderHook, type RenderHookOptions, type RenderHookResult } from '#/test'
import { describe, expect, it } from 'vitest'
import { useStore } from 'zustand'
import { DirectoryId } from '../../services/Backend'
import DriveProvider, { useDriveStore } from '../DriveProvider'

function renderDriveProviderHook<Result, Props>(
hook: (props: Props) => Result,
options?: Omit<RenderHookOptions<Props>, 'wrapper'>,
): RenderHookResult<Result, Props> {
return renderHook(hook, { wrapper: DriveProvider, ...options })
}

describe('<DriveProvider />', () => {
it('Should reset expanded directory ids when category changes', () => {
const driveAPI = renderDriveProviderHook(() => {
const store = useDriveStore()
return useStore(store, ({ setCategory, setExpandedDirectoryIds, expandedDirectoryIds }) => ({
expandedDirectoryIds,
setCategory,
setExpandedDirectoryIds,
}))
})

act(() => {
driveAPI.result.current.setExpandedDirectoryIds([DirectoryId('test-123')])
})

expect(driveAPI.result.current.expandedDirectoryIds).toEqual([DirectoryId('test-123')])

act(() => {
driveAPI.result.current.setCategory({} as Category)
})

expect(driveAPI.result.current.expandedDirectoryIds).toEqual([])
})
})
7 changes: 7 additions & 0 deletions app/gui/src/dashboard/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @file
*
* Barrel files for utility renderes
*/

export * from './testUtils'
16 changes: 16 additions & 0 deletions app/gui/src/dashboard/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @file Global setup for dashboard tests.
*/

import * as matchers from '@testing-library/jest-dom/matchers'
import { cleanup } from '@testing-library/react'
import { MotionGlobalConfig } from 'framer-motion'
import { afterEach, expect } from 'vitest'

MotionGlobalConfig.skipAnimations = true

expect.extend(matchers)

afterEach(() => {
cleanup()
})
115 changes: 115 additions & 0 deletions app/gui/src/dashboard/test/testUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @file Utility functions for testing.
*
* **IMPORTANT**: This file is supposed to be used instead of `@testing-library/react`
* It is used to provide a portal root and locale to all tests.
*/

import { Form, type FormProps, type TSchema } from '#/components/AriaComponents'
import UIProviders from '#/components/UIProviders'
import {
render,
renderHook,
type RenderHookOptions,
type RenderHookResult,
type RenderOptions,
type RenderResult,
} from '@testing-library/react'
import { type PropsWithChildren, type ReactElement } from 'react'

/**
* A wrapper that passes through its children.
*/
function PassThroughWrapper({ children }: PropsWithChildren) {
return children
}

/**
* A wrapper that provides the {@link UIProviders} context.
*/
function UIProvidersWrapper({ children }: PropsWithChildren) {
return (
<UIProviders portalRoot={document.body} locale="en">
{children}
</UIProviders>
)
}

/**
* A wrapper that provides the {@link Form} context.
*/
function FormWrapper<Schema extends TSchema, SubmitResult = void>(
props: FormProps<Schema, SubmitResult>,
) {
return <Form {...props} />
}

/**
* Custom render function for tests.
*/
function renderWithRoot(ui: ReactElement, options?: Omit<RenderOptions, 'queries'>): RenderResult {
const { wrapper: Wrapper = PassThroughWrapper, ...rest } = options ?? {}

return render(ui, {
wrapper: ({ children }) => (
<UIProvidersWrapper>
<Wrapper>{children}</Wrapper>
</UIProvidersWrapper>
),
...rest,
})
}

/**
* Adds a form wrapper to the component.
*/
function renderWithForm<Schema extends TSchema, SubmitResult = void>(
ui: ReactElement,
options: Omit<RenderOptions, 'queries' | 'wrapper'> & {
formProps: FormProps<Schema, SubmitResult>
},
): RenderResult {
const { formProps, ...rest } = options

return renderWithRoot(ui, {
wrapper: ({ children }) => <FormWrapper {...formProps}>{children}</FormWrapper>,
...rest,
})
}

/**
* A custom renderHook function for tests.
*/
function renderHookWithRoot<Result, Props>(
hook: (props: Props) => Result,
options?: Omit<RenderHookOptions<Props>, 'queries'>,
): RenderHookResult<Result, Props> {
return renderHook(hook, { wrapper: UIProvidersWrapper, ...options })
}

/**
* A custom renderHook function for tests that provides the {@link Form} context.
*/
function renderHookWithForm<Result, Props, Schema extends TSchema, SubmitResult = void>(
hook: (props: Props) => Result,
options: Omit<RenderHookOptions<Props>, 'queries' | 'wrapper'> & {
formProps: FormProps<Schema, SubmitResult>
},
): RenderHookResult<Result, Props> {
const { formProps, ...rest } = options

return renderHookWithRoot(hook, {
wrapper: ({ children }) => <FormWrapper {...formProps}>{children}</FormWrapper>,
...rest,
})
}

export * from '@testing-library/react'
export { default as userEvent } from '@testing-library/user-event'
// override render method
export {
renderWithRoot as render,
renderHookWithRoot as renderHook,
renderHookWithForm,
renderWithForm,
}
2 changes: 0 additions & 2 deletions app/gui/src/dashboard/utilities/__tests__/dateTime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import * as dateTime from '#/utilities/dateTime'
// === Tests ===
// =============

/* eslint-disable @typescript-eslint/no-magic-numbers */

/** The number of milliseconds in a minute. */
const MIN_MS = 60_000
/** Remove all UTC offset from a {@link Date}. Daylight savings-aware. */
Expand Down
10 changes: 4 additions & 6 deletions app/gui/src/dashboard/utilities/__tests__/jsonSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,17 @@ fc.test.prop({ value: fc.fc.float() })('number schema', ({ value }) => {

fc.test.prop({
value: fc.fc.float().filter((n) => n > 0),
// eslint-disable-next-line @typescript-eslint/no-magic-numbers

multiplier: fc.fc.integer({ min: -1_000_000, max: 1_000_000 }),
})('number multiples', ({ value, multiplier }) => {
const schema = { type: 'number', multipleOf: value }
if (Number.isFinite(value)) {
v.expect(AJV.validate(schema, 0)).toBe(true)
v.expect(AJV.validate(schema, value)).toBe(true)
// eslint-disable-next-line @typescript-eslint/no-magic-numbers

if (Math.abs(value * (multiplier + 0.5)) < Number.MAX_SAFE_INTEGER) {
v.expect(AJV.validate(schema, value * multiplier)).toBe(true)
if (value !== 0) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
v.expect(AJV.validate(schema, value * (multiplier + 0.5))).toBe(false)
}
}
Expand All @@ -99,17 +98,16 @@ fc.test.prop({ value: fc.fc.integer() })('integer schema', ({ value }) => {

fc.test.prop({
value: fc.fc.integer().filter((n) => n > 0),
// eslint-disable-next-line @typescript-eslint/no-magic-numbers

multiplier: fc.fc.integer({ min: -1_000_000, max: 1_000_000 }),
})('integer multiples', ({ value, multiplier }) => {
const schema = { type: 'integer', multipleOf: value }
v.expect(AJV.validate(schema, 0)).toBe(true)
v.expect(AJV.validate(schema, value)).toBe(true)
// eslint-disable-next-line @typescript-eslint/no-magic-numbers

if (Math.abs(value * (multiplier + 0.5)) < Number.MAX_SAFE_INTEGER) {
v.expect(AJV.validate(schema, value * multiplier)).toBe(true)
if (value !== 0) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
v.expect(AJV.validate(schema, value * (multiplier + 0.5))).toBe(false)
}
}
Expand Down
11 changes: 6 additions & 5 deletions app/gui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { defineConfig, type Plugin } from 'vite'
import VueDevTools from 'vite-plugin-vue-devtools'
import wasm from 'vite-plugin-wasm'
import tailwindConfig from './tailwind.config'
// @ts-expect-error We don't need to typecheck this file
import reactCompiler from 'babel-plugin-react-compiler'
// @ts-expect-error We don't need to typecheck this file
import syntaxImportAttributes from '@babel/plugin-syntax-import-attributes'

const dynHostnameWsUrl = (port: number) => JSON.stringify(`ws://__HOSTNAME__:${port}`)
const projectManagerUrl = dynHostnameWsUrl(process.env.INTEGRATION_TEST === 'true' ? 30536 : 30535)
Expand Down Expand Up @@ -57,11 +61,8 @@ export default defineConfig({
include: fileURLToPath(new URL('./src/dashboard/**/*.tsx', import.meta.url)),
babel: {
plugins: [
'@babel/plugin-syntax-import-attributes',
[
'babel-plugin-react-compiler',
{ target: '18', enablePreserveExistingMemoizationGuarantees: true },
],
syntaxImportAttributes,
[reactCompiler, { target: '18', enablePreserveExistingMemoizationGuarantees: true }],
],
},
}),
Expand Down
3 changes: 2 additions & 1 deletion app/gui/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ const config = mergeConfig(
defineConfig({
test: {
environment: 'jsdom',
includeSource: ['./src/**/*.{ts,vue}'],
includeSource: ['./src/**/*.{ts,tsx,vue}'],
exclude: [...configDefaults.exclude, 'integration-test/**/*'],
root: fileURLToPath(new URL('./', import.meta.url)),
restoreMocks: true,
setupFiles: './src/dashboard/test/setup.ts',
},
}),
)
Expand Down
Loading

0 comments on commit 1676545

Please sign in to comment.