Skip to content

Commit 1676545

Browse files
Reset expanded directories list on category change (#11725)
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
1 parent 2894618 commit 1676545

19 files changed

+324
-42
lines changed

app/common/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
"scripts": {
3131
"test": "vitest run",
32-
"lint": "eslint . --cache --max-warnings=0"
32+
"lint": "eslint ./src --cache --max-warnings=0"
3333
},
3434
"peerDependencies": {
3535
"@tanstack/query-core": "5.54.1",

app/gui/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
"@fast-check/vitest": "^0.0.8",
131131
"@modyfi/vite-plugin-yaml": "^1.0.4",
132132
"@playwright/test": "^1.40.0",
133+
"@babel/plugin-syntax-import-attributes": "^7.24.7",
133134
"@react-types/shared": "^3.22.1",
134135
"@storybook/addon-essentials": "^8.4.2",
135136
"@storybook/addon-interactions": "^8.4.2",
@@ -179,6 +180,10 @@
179180
"@vitest/coverage-v8": "^1.3.1",
180181
"@vue/test-utils": "^2.4.6",
181182
"@vue/tsconfig": "^0.5.1",
183+
"@testing-library/jest-dom": "6.6.3",
184+
"@testing-library/react": "16.0.1",
185+
"@testing-library/user-event": "14.5.2",
186+
"@testing-library/react-hooks": "8.0.1",
182187
"css.escape": "^1.5.1",
183188
"d3": "^7.4.0",
184189
"enso-common": "workspace:*",

app/gui/src/dashboard/components/AriaComponents/Button/Button.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,8 @@ export const BUTTON_STYLES = tv({
272272
{ size: 'medium', iconOnly: true, class: { base: 'p-0 rounded-full', icon: 'w-4 h-4' } },
273273
{ size: 'large', iconOnly: true, class: { base: 'p-0 rounded-full', icon: 'w-4.5 h-4.5' } },
274274
{ size: 'hero', iconOnly: true, class: { base: 'p-0 rounded-full', icon: 'w-12 h-12' } },
275-
{ fullWidth: false, class: { icon: 'flex-none' } },
275+
276+
{ variant: 'icon', class: { base: 'flex-none' } },
276277

277278
{ variant: 'link', isFocused: true, class: 'focus-visible:outline-offset-1' },
278279
{ variant: 'link', size: 'xxsmall', class: 'font-medium' },

app/gui/src/dashboard/components/AriaComponents/CopyBlock/CopyBlock.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import * as copyHook from '#/hooks/copyHooks'
66

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

9-
import * as ariaComponents from '#/components/AriaComponents'
10-
119
import * as twv from '#/utilities/tailwindVariants'
10+
import { Button } from '../Button'
11+
import { TEXT_STYLE } from '../Text'
1212

1313
// =================
1414
// === Constants ===
1515
// =================
1616

1717
const COPY_BLOCK_STYLES = twv.tv({
18-
base: ariaComponents.TEXT_STYLE({
18+
base: TEXT_STYLE({
1919
class: 'max-w-full bg-primary/5 border-primary/10',
2020
}),
2121
variants: {
@@ -58,14 +58,14 @@ export function CopyBlock(props: CopyBlockProps) {
5858
const { copyTextBlock, base } = COPY_BLOCK_STYLES()
5959

6060
return (
61-
<ariaComponents.Button
61+
<Button
6262
variant="custom"
6363
size="custom"
6464
onPress={() => mutateAsync()}
6565
tooltip={isSuccess ? getText('copied') : getText('copy')}
6666
className={base({ className })}
6767
>
6868
<span className={copyTextBlock()}>{copyText}</span>
69-
</ariaComponents.Button>
69+
</Button>
7070
)
7171
}

app/gui/src/dashboard/components/dashboard/ProjectIcon.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export default function ProjectIcon(props: ProjectIconProps) {
219219
icon={StopIcon}
220220
aria-label={userOpeningProjectTooltip ?? getText('stopExecution')}
221221
tooltipPlacement="left"
222-
className={tailwindMerge.twMerge(isRunningInBackground && 'text-green')}
222+
className={tailwindMerge.twJoin(isRunningInBackground && 'text-green')}
223223
onPress={doCloseProject}
224224
/>
225225
<Spinner

app/gui/src/dashboard/layouts/AssetsTable/assetTreeHooks.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useMemo } from 'react'
33

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

6+
import type { DirectoryId } from 'enso-common/src/services/Backend'
67
import {
78
assetIsDirectory,
89
createRootDirectoryAsset,
@@ -11,7 +12,6 @@ import {
1112
createSpecialLoadingAsset,
1213
type AnyAsset,
1314
type DirectoryAsset,
14-
type DirectoryId,
1515
} from 'enso-common/src/services/Backend'
1616

1717
import { listDirectoryQueryOptions } from '#/hooks/backendHooks'
@@ -88,10 +88,13 @@ export function useAssetTree(options: UseAssetTreeOptions) {
8888
useMemo(
8989
() => ({
9090
queryKey: [backend.type, 'refetchListDirectory'],
91-
queryFn: async () => {
92-
await queryClient.refetchQueries({ queryKey: [backend.type, 'listDirectory'] })
93-
return null
94-
},
91+
queryFn: () =>
92+
queryClient
93+
.refetchQueries({
94+
queryKey: [backend.type, 'listDirectory'],
95+
type: 'active',
96+
})
97+
.then(() => null),
9598
refetchInterval:
9699
enableAssetsTableBackgroundRefresh ? assetsTableBackgroundRefreshInterval : false,
97100
refetchOnMount: 'always',

app/gui/src/dashboard/providers/DriveProvider.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export default function DriveProvider(props: ProjectsProviderProps) {
8383
targetDirectory: null,
8484
selectedKeys: EMPTY_SET,
8585
visuallySelectedKeys: null,
86+
expandedDirectoryIds: EMPTY_ARRAY,
8687
})
8788
}
8889
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Category } from '#/layouts/CategorySwitcher/Category'
2+
import { act, renderHook, type RenderHookOptions, type RenderHookResult } from '#/test'
3+
import { describe, expect, it } from 'vitest'
4+
import { useStore } from 'zustand'
5+
import { DirectoryId } from '../../services/Backend'
6+
import DriveProvider, { useDriveStore } from '../DriveProvider'
7+
8+
function renderDriveProviderHook<Result, Props>(
9+
hook: (props: Props) => Result,
10+
options?: Omit<RenderHookOptions<Props>, 'wrapper'>,
11+
): RenderHookResult<Result, Props> {
12+
return renderHook(hook, { wrapper: DriveProvider, ...options })
13+
}
14+
15+
describe('<DriveProvider />', () => {
16+
it('Should reset expanded directory ids when category changes', () => {
17+
const driveAPI = renderDriveProviderHook(() => {
18+
const store = useDriveStore()
19+
return useStore(store, ({ setCategory, setExpandedDirectoryIds, expandedDirectoryIds }) => ({
20+
expandedDirectoryIds,
21+
setCategory,
22+
setExpandedDirectoryIds,
23+
}))
24+
})
25+
26+
act(() => {
27+
driveAPI.result.current.setExpandedDirectoryIds([DirectoryId('test-123')])
28+
})
29+
30+
expect(driveAPI.result.current.expandedDirectoryIds).toEqual([DirectoryId('test-123')])
31+
32+
act(() => {
33+
driveAPI.result.current.setCategory({} as Category)
34+
})
35+
36+
expect(driveAPI.result.current.expandedDirectoryIds).toEqual([])
37+
})
38+
})

app/gui/src/dashboard/test/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* @file
3+
*
4+
* Barrel files for utility renderes
5+
*/
6+
7+
export * from './testUtils'

app/gui/src/dashboard/test/setup.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @file Global setup for dashboard tests.
3+
*/
4+
5+
import * as matchers from '@testing-library/jest-dom/matchers'
6+
import { cleanup } from '@testing-library/react'
7+
import { MotionGlobalConfig } from 'framer-motion'
8+
import { afterEach, expect } from 'vitest'
9+
10+
MotionGlobalConfig.skipAnimations = true
11+
12+
expect.extend(matchers)
13+
14+
afterEach(() => {
15+
cleanup()
16+
})
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @file Utility functions for testing.
3+
*
4+
* **IMPORTANT**: This file is supposed to be used instead of `@testing-library/react`
5+
* It is used to provide a portal root and locale to all tests.
6+
*/
7+
8+
import { Form, type FormProps, type TSchema } from '#/components/AriaComponents'
9+
import UIProviders from '#/components/UIProviders'
10+
import {
11+
render,
12+
renderHook,
13+
type RenderHookOptions,
14+
type RenderHookResult,
15+
type RenderOptions,
16+
type RenderResult,
17+
} from '@testing-library/react'
18+
import { type PropsWithChildren, type ReactElement } from 'react'
19+
20+
/**
21+
* A wrapper that passes through its children.
22+
*/
23+
function PassThroughWrapper({ children }: PropsWithChildren) {
24+
return children
25+
}
26+
27+
/**
28+
* A wrapper that provides the {@link UIProviders} context.
29+
*/
30+
function UIProvidersWrapper({ children }: PropsWithChildren) {
31+
return (
32+
<UIProviders portalRoot={document.body} locale="en">
33+
{children}
34+
</UIProviders>
35+
)
36+
}
37+
38+
/**
39+
* A wrapper that provides the {@link Form} context.
40+
*/
41+
function FormWrapper<Schema extends TSchema, SubmitResult = void>(
42+
props: FormProps<Schema, SubmitResult>,
43+
) {
44+
return <Form {...props} />
45+
}
46+
47+
/**
48+
* Custom render function for tests.
49+
*/
50+
function renderWithRoot(ui: ReactElement, options?: Omit<RenderOptions, 'queries'>): RenderResult {
51+
const { wrapper: Wrapper = PassThroughWrapper, ...rest } = options ?? {}
52+
53+
return render(ui, {
54+
wrapper: ({ children }) => (
55+
<UIProvidersWrapper>
56+
<Wrapper>{children}</Wrapper>
57+
</UIProvidersWrapper>
58+
),
59+
...rest,
60+
})
61+
}
62+
63+
/**
64+
* Adds a form wrapper to the component.
65+
*/
66+
function renderWithForm<Schema extends TSchema, SubmitResult = void>(
67+
ui: ReactElement,
68+
options: Omit<RenderOptions, 'queries' | 'wrapper'> & {
69+
formProps: FormProps<Schema, SubmitResult>
70+
},
71+
): RenderResult {
72+
const { formProps, ...rest } = options
73+
74+
return renderWithRoot(ui, {
75+
wrapper: ({ children }) => <FormWrapper {...formProps}>{children}</FormWrapper>,
76+
...rest,
77+
})
78+
}
79+
80+
/**
81+
* A custom renderHook function for tests.
82+
*/
83+
function renderHookWithRoot<Result, Props>(
84+
hook: (props: Props) => Result,
85+
options?: Omit<RenderHookOptions<Props>, 'queries'>,
86+
): RenderHookResult<Result, Props> {
87+
return renderHook(hook, { wrapper: UIProvidersWrapper, ...options })
88+
}
89+
90+
/**
91+
* A custom renderHook function for tests that provides the {@link Form} context.
92+
*/
93+
function renderHookWithForm<Result, Props, Schema extends TSchema, SubmitResult = void>(
94+
hook: (props: Props) => Result,
95+
options: Omit<RenderHookOptions<Props>, 'queries' | 'wrapper'> & {
96+
formProps: FormProps<Schema, SubmitResult>
97+
},
98+
): RenderHookResult<Result, Props> {
99+
const { formProps, ...rest } = options
100+
101+
return renderHookWithRoot(hook, {
102+
wrapper: ({ children }) => <FormWrapper {...formProps}>{children}</FormWrapper>,
103+
...rest,
104+
})
105+
}
106+
107+
export * from '@testing-library/react'
108+
export { default as userEvent } from '@testing-library/user-event'
109+
// override render method
110+
export {
111+
renderWithRoot as render,
112+
renderHookWithRoot as renderHook,
113+
renderHookWithForm,
114+
renderWithForm,
115+
}

app/gui/src/dashboard/utilities/__tests__/dateTime.test.ts

-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import * as dateTime from '#/utilities/dateTime'
77
// === Tests ===
88
// =============
99

10-
/* eslint-disable @typescript-eslint/no-magic-numbers */
11-
1210
/** The number of milliseconds in a minute. */
1311
const MIN_MS = 60_000
1412
/** Remove all UTC offset from a {@link Date}. Daylight savings-aware. */

app/gui/src/dashboard/utilities/__tests__/jsonSchema.test.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,17 @@ fc.test.prop({ value: fc.fc.float() })('number schema', ({ value }) => {
7171

7272
fc.test.prop({
7373
value: fc.fc.float().filter((n) => n > 0),
74-
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
74+
7575
multiplier: fc.fc.integer({ min: -1_000_000, max: 1_000_000 }),
7676
})('number multiples', ({ value, multiplier }) => {
7777
const schema = { type: 'number', multipleOf: value }
7878
if (Number.isFinite(value)) {
7979
v.expect(AJV.validate(schema, 0)).toBe(true)
8080
v.expect(AJV.validate(schema, value)).toBe(true)
81-
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
81+
8282
if (Math.abs(value * (multiplier + 0.5)) < Number.MAX_SAFE_INTEGER) {
8383
v.expect(AJV.validate(schema, value * multiplier)).toBe(true)
8484
if (value !== 0) {
85-
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
8685
v.expect(AJV.validate(schema, value * (multiplier + 0.5))).toBe(false)
8786
}
8887
}
@@ -99,17 +98,16 @@ fc.test.prop({ value: fc.fc.integer() })('integer schema', ({ value }) => {
9998

10099
fc.test.prop({
101100
value: fc.fc.integer().filter((n) => n > 0),
102-
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
101+
103102
multiplier: fc.fc.integer({ min: -1_000_000, max: 1_000_000 }),
104103
})('integer multiples', ({ value, multiplier }) => {
105104
const schema = { type: 'integer', multipleOf: value }
106105
v.expect(AJV.validate(schema, 0)).toBe(true)
107106
v.expect(AJV.validate(schema, value)).toBe(true)
108-
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
107+
109108
if (Math.abs(value * (multiplier + 0.5)) < Number.MAX_SAFE_INTEGER) {
110109
v.expect(AJV.validate(schema, value * multiplier)).toBe(true)
111110
if (value !== 0) {
112-
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
113111
v.expect(AJV.validate(schema, value * (multiplier + 0.5))).toBe(false)
114112
}
115113
}

app/gui/vite.config.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { defineConfig, type Plugin } from 'vite'
1212
import VueDevTools from 'vite-plugin-vue-devtools'
1313
import wasm from 'vite-plugin-wasm'
1414
import tailwindConfig from './tailwind.config'
15+
// @ts-expect-error We don't need to typecheck this file
16+
import reactCompiler from 'babel-plugin-react-compiler'
17+
// @ts-expect-error We don't need to typecheck this file
18+
import syntaxImportAttributes from '@babel/plugin-syntax-import-attributes'
1519

1620
const dynHostnameWsUrl = (port: number) => JSON.stringify(`ws://__HOSTNAME__:${port}`)
1721
const projectManagerUrl = dynHostnameWsUrl(process.env.INTEGRATION_TEST === 'true' ? 30536 : 30535)
@@ -57,11 +61,8 @@ export default defineConfig({
5761
include: fileURLToPath(new URL('./src/dashboard/**/*.tsx', import.meta.url)),
5862
babel: {
5963
plugins: [
60-
'@babel/plugin-syntax-import-attributes',
61-
[
62-
'babel-plugin-react-compiler',
63-
{ target: '18', enablePreserveExistingMemoizationGuarantees: true },
64-
],
64+
syntaxImportAttributes,
65+
[reactCompiler, { target: '18', enablePreserveExistingMemoizationGuarantees: true }],
6566
],
6667
},
6768
}),

app/gui/vitest.config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ const config = mergeConfig(
77
defineConfig({
88
test: {
99
environment: 'jsdom',
10-
includeSource: ['./src/**/*.{ts,vue}'],
10+
includeSource: ['./src/**/*.{ts,tsx,vue}'],
1111
exclude: [...configDefaults.exclude, 'integration-test/**/*'],
1212
root: fileURLToPath(new URL('./', import.meta.url)),
1313
restoreMocks: true,
14+
setupFiles: './src/dashboard/test/setup.ts',
1415
},
1516
}),
1617
)

0 commit comments

Comments
 (0)