From 8feab45299fbdfe03f9e1c48dc81e8ff141b9e1d Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 27 Feb 2023 13:15:47 +0800 Subject: [PATCH] UI testing (#565) * store demo data in React Query * Playwright for UI screenshots * use path based routing on web * Fix Typescript error --- .gitignore | 2 + apps/desktop/package.json | 1 + apps/desktop/src/App.tsx | 13 +- apps/mobile/src/App.tsx | 5 +- .../components/dialog/CreateLibraryDialog.tsx | 4 +- .../explorer/sections/FavoriteButton.tsx | 4 +- .../confirm-modals/DeleteLibraryModal.tsx | 6 +- .../components/modal/tag/CreateTagModal.tsx | 4 +- .../components/modal/tag/UpdateTagModal.tsx | 6 +- apps/web/package.json | 2 + apps/web/playwright.config.ts | 60 ++++++ apps/web/screenshots/overview-dark.png | Bin 0 -> 24788 bytes apps/web/screenshots/overview-light.png | Bin 0 -> 23405 bytes apps/web/src/App.tsx | 29 ++- apps/web/src/demoData.json | 182 ++++++++++++++++++ apps/web/src/index.tsx | 1 + apps/web/tests/screenshots.test.ts | 13 ++ apps/web/tsconfig.json | 2 +- packages/client/src/rspc.ts | 2 - packages/interface/src/App.tsx | 21 +- .../src/components/layout/Sidebar.tsx | 14 ++ packages/interface/src/screens/Overview.tsx | 2 + pnpm-lock.yaml | Bin 787293 -> 788557 bytes 23 files changed, 344 insertions(+), 29 deletions(-) create mode 100644 apps/web/playwright.config.ts create mode 100644 apps/web/screenshots/overview-dark.png create mode 100644 apps/web/screenshots/overview-light.png create mode 100644 apps/web/src/demoData.json create mode 100644 apps/web/tests/screenshots.test.ts diff --git a/.gitignore b/.gitignore index 907c91f7b..652f563a3 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,8 @@ examples/*/*.lock /target prisma*.rs +playwright-report + /sdserver_data .spacedrive dev.db-journal diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0a384195e..4f2ed4bb0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,6 +18,7 @@ "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", + "@tanstack/react-query": "^4.24.4", "@tauri-apps/api": "1.2.0", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 04e979687..92c63dad1 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,13 +1,13 @@ import { loggerLink } from '@rspc/client'; import { tauriLink } from '@rspc/tauri'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { dialog, invoke, os, shell } from '@tauri-apps/api'; import { listen } from '@tauri-apps/api/event'; import { convertFileSrc } from '@tauri-apps/api/tauri'; import { useEffect } from 'react'; -import { getDebugState, hooks, queryClient } from '@sd/client'; +import { getDebugState, hooks } from '@sd/client'; import SpacedriveInterface, { OperatingSystem, Platform, PlatformProvider } from '@sd/interface'; -import { KeybindEvent } from '@sd/interface'; -import { ErrorPage } from '@sd/interface'; +import { KeybindEvent, ErrorPage } from '@sd/interface'; import '@sd/ui/style'; const client = hooks.createClient({ @@ -65,6 +65,8 @@ const platform: Platform = { openPath: (path) => shell.open(path) }; +const queryClient = new QueryClient(); + export default function App() { useEffect(() => { // This tells Tauri to show the current window because it's finished loading @@ -86,9 +88,12 @@ export default function App() { } return ( + // @ts-expect-error: Just a version mismatch - + + + ); diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 81ddcf4b6..c3138b00a 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -1,6 +1,7 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import { DefaultTheme, NavigationContainer, Theme } from '@react-navigation/native'; import { loggerLink } from '@rspc/client'; +import { QueryClient } from '@tanstack/react-query'; import dayjs from 'dayjs'; import advancedFormat from 'dayjs/plugin/advancedFormat'; import duration from 'dayjs/plugin/duration'; @@ -17,7 +18,6 @@ import { ClientContextProvider, LibraryContextProvider, getDebugState, - queryClient, rspc, useClientContext, useInvalidateQuery @@ -95,12 +95,15 @@ const client = rspc.createClient({ ] }); +const queryClient = new QueryClient(); + export default function App() { useEffect(() => { SplashScreen.hideAsync(); }, []); return ( + // @ts-expect-error: Version mismatch diff --git a/apps/mobile/src/components/dialog/CreateLibraryDialog.tsx b/apps/mobile/src/components/dialog/CreateLibraryDialog.tsx index b71768502..8604154d1 100644 --- a/apps/mobile/src/components/dialog/CreateLibraryDialog.tsx +++ b/apps/mobile/src/components/dialog/CreateLibraryDialog.tsx @@ -1,5 +1,6 @@ +import { useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; -import { queryClient, useBridgeMutation } from '@sd/client'; +import { useBridgeMutation } from '@sd/client'; import { Input } from '~/components/form/Input'; import Dialog from '~/components/layout/Dialog'; import { currentLibraryStore } from '~/utils/nav'; @@ -12,6 +13,7 @@ type Props = { // TODO: Move to a Modal component const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props) => { + const queryClient = useQueryClient(); const [libName, setLibName] = useState(''); const [isOpen, setIsOpen] = useState(false); diff --git a/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx b/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx index d3353a0cc..6ed18f47f 100644 --- a/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx +++ b/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx @@ -1,7 +1,8 @@ import { Heart } from 'phosphor-react-native'; import { useState } from 'react'; import { Pressable, PressableProps } from 'react-native'; -import { Object as SDObject, queryClient, useLibraryMutation } from '@sd/client'; +import { Object as SDObject, useLibraryMutation } from '@sd/client'; +import { useQueryClient } from '@tanstack/react-query'; type Props = { data: SDObject; @@ -9,6 +10,7 @@ type Props = { }; const FavoriteButton = (props: Props) => { + const queryClient = useQueryClient(); const [favorite, setFavorite] = useState(props.data.favorite); const { mutate: toggleFavorite, isLoading } = useLibraryMutation('files.setFavorite', { diff --git a/apps/mobile/src/components/modal/confirm-modals/DeleteLibraryModal.tsx b/apps/mobile/src/components/modal/confirm-modals/DeleteLibraryModal.tsx index 26a5f64ca..b7f04b7c8 100644 --- a/apps/mobile/src/components/modal/confirm-modals/DeleteLibraryModal.tsx +++ b/apps/mobile/src/components/modal/confirm-modals/DeleteLibraryModal.tsx @@ -1,6 +1,7 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useBridgeMutation } from '@sd/client'; import { useRef } from 'react'; -import { queryClient, useBridgeMutation } from '@sd/client'; -import { ConfirmModal, ModalRef } from '~/components/layout/Modal'; +import { ModalRef, ConfirmModal } from '~/components/layout/Modal'; type Props = { libraryUuid: string; @@ -9,6 +10,7 @@ type Props = { }; const DeleteLibraryModal = ({ trigger, onSubmit, libraryUuid }: Props) => { + const queryClient = useQueryClient(); const modalRef = useRef(null); const { mutate: deleteLibrary, isLoading: deleteLibLoading } = useBridgeMutation( diff --git a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx index 17c6aa5bb..aca5ec983 100644 --- a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx +++ b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx @@ -1,15 +1,17 @@ import { forwardRef, useEffect, useState } from 'react'; import { Pressable, Text, View } from 'react-native'; import ColorPicker from 'react-native-wheel-color-picker'; -import { queryClient, useLibraryMutation } from '@sd/client'; +import { useLibraryMutation } from '@sd/client'; import { FadeInAnimation } from '~/components/animation/layout'; import { Input } from '~/components/form/Input'; import { Modal, ModalRef } from '~/components/layout/Modal'; import { Button } from '~/components/primitive/Button'; import useForwardedRef from '~/hooks/useForwardedRef'; import { tw, twStyle } from '~/lib/tailwind'; +import { useQueryClient } from '@tanstack/react-query'; const CreateTagModal = forwardRef((_, ref) => { + const queryClient = useQueryClient(); const modalRef = useForwardedRef(ref); const [tagName, setTagName] = useState(''); diff --git a/apps/mobile/src/components/modal/tag/UpdateTagModal.tsx b/apps/mobile/src/components/modal/tag/UpdateTagModal.tsx index 3cf50ac1d..703fe4967 100644 --- a/apps/mobile/src/components/modal/tag/UpdateTagModal.tsx +++ b/apps/mobile/src/components/modal/tag/UpdateTagModal.tsx @@ -1,6 +1,6 @@ import { forwardRef, useEffect, useState } from 'react'; -import { ColorValue, Pressable, Text, View } from 'react-native'; -import { Tag, queryClient, useLibraryMutation } from '@sd/client'; +import { Pressable, Text, View } from 'react-native'; +import { Tag, useLibraryMutation } from '@sd/client'; import { FadeInAnimation } from '~/components/animation/layout'; import ColorPicker from '~/components/form/ColorPicker'; import { Input } from '~/components/form/Input'; @@ -8,6 +8,7 @@ import { Modal, ModalRef } from '~/components/layout/Modal'; import { Button } from '~/components/primitive/Button'; import useForwardedRef from '~/hooks/useForwardedRef'; import { tw, twStyle } from '~/lib/tailwind'; +import { useQueryClient } from '@tanstack/react-query'; type Props = { tag: Tag; @@ -15,6 +16,7 @@ type Props = { }; const UpdateTagModal = forwardRef((props, ref) => { + const queryClient = useQueryClient(); const modalRef = useForwardedRef(ref); const [tagName, setTagName] = useState(props.tag.name!); diff --git a/apps/web/package.json b/apps/web/package.json index e94d515eb..7fd03b7b0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,6 +6,7 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", + "test": "VITE_SD_DEMO_MODE=true playwright test", "typecheck": "tsc -b" }, "dependencies": { @@ -18,6 +19,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@playwright/test": "^1.30.0", "@sd/config": "workspace:*", "@sd/ui": "workspace:*", "@types/react": "^18.0.21", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts new file mode 100644 index 000000000..221c95d16 --- /dev/null +++ b/apps/web/playwright.config.ts @@ -0,0 +1,60 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + timeout: 30 * 1000, + expect: { + timeout: 5000 + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + webServer: { + command: 'pnpm build && pnpm preview', + port: 4173 + }, + use: { + actionTimeout: 0, + trace: 'on-first-retry' + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + } + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] } + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] } + // } + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { channel: 'chrome' }, + // }, + ] +}); diff --git a/apps/web/screenshots/overview-dark.png b/apps/web/screenshots/overview-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..91b0a3b20a340c305d72081d7e20fdaa82a32e37 GIT binary patch literal 24788 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 window.open(url, '_blank')?.focus(), - demoMode: true + demoMode: import.meta.env.VITE_SD_DEMO_MODE === 'true' }; +const queryClient = new QueryClient({ + defaultOptions: { + queries: import.meta.env.VITE_SD_DEMO_MODE + ? { + refetchOnWindowFocus: false, + staleTime: Infinity, + cacheTime: Infinity, + networkMode: 'offlineFirst', + enabled: false + } + : undefined + // TODO: Mutations can't be globally disable which is annoying! + } +}); + function App() { useEffect(() => window.parent.postMessage('spacedrive-hello', '*'), []); + if (import.meta.env.VITE_SD_DEMO_MODE === 'true') { + hydrate(queryClient, demoData); + } + return (
- + + +
diff --git a/apps/web/src/demoData.json b/apps/web/src/demoData.json new file mode 100644 index 000000000..4bf298eb5 --- /dev/null +++ b/apps/web/src/demoData.json @@ -0,0 +1,182 @@ +{ + "mutations": [], + "queries": [ + { + "state": { + "data": [ + { + "uuid": "dc1c41b9-65c6-4a9d-a418-79e628b3ac59", + "config": { "version": "0.1.0", "name": "My Library", "description": "" } + } + ], + "dataUpdateCount": 2, + "dataUpdatedAt": 1675912735380, + "error": null, + "errorUpdateCount": 0, + "errorUpdatedAt": 0, + "fetchFailureCount": 0, + "fetchFailureReason": null, + "fetchMeta": {}, + "isInvalidated": false, + "status": "success", + "fetchStatus": "fetching" + }, + "queryKey": ["library.list"], + "queryHash": "[\"library.list\"]" + }, + { + "state": { + "data": "macOS", + "dataUpdateCount": 0, + "dataUpdatedAt": 1675912734876, + "error": null, + "errorUpdateCount": 0, + "errorUpdatedAt": 0, + "fetchFailureCount": 0, + "fetchFailureReason": null, + "fetchMeta": null, + "isInvalidated": false, + "status": "success", + "fetchStatus": "idle" + }, + "queryKey": ["_tauri", "platform"], + "queryHash": "[\"_tauri\",\"platform\"]" + }, + { + "state": { + "data": [], + "dataUpdateCount": 1, + "dataUpdatedAt": 1675912735051, + "error": null, + "errorUpdateCount": 0, + "errorUpdatedAt": 0, + "fetchFailureCount": 0, + "fetchFailureReason": null, + "fetchMeta": {}, + "isInvalidated": false, + "status": "success", + "fetchStatus": "fetching" + }, + "queryKey": [ + "locations.list", + { "library_id": "dc1c41b9-65c6-4a9d-a418-79e628b3ac59", "arg": null } + ], + "queryHash": "[\"locations.list\",{\"arg\":null,\"library_id\":\"dc1c41b9-65c6-4a9d-a418-79e628b3ac59\"}]" + }, + { + "state": { + "data": [], + "dataUpdateCount": 1, + "dataUpdatedAt": 1675912735057, + "error": null, + "errorUpdateCount": 0, + "errorUpdatedAt": 0, + "fetchFailureCount": 0, + "fetchFailureReason": null, + "fetchMeta": {}, + "isInvalidated": false, + "status": "success", + "fetchStatus": "fetching" + }, + "queryKey": [ + "tags.list", + { "library_id": "dc1c41b9-65c6-4a9d-a418-79e628b3ac59", "arg": null } + ], + "queryHash": "[\"tags.list\",{\"arg\":null,\"library_id\":\"dc1c41b9-65c6-4a9d-a418-79e628b3ac59\"}]" + }, + { + "state": { + "data": false, + "dataUpdateCount": 1, + "dataUpdatedAt": 1675912735057, + "error": null, + "errorUpdateCount": 0, + "errorUpdatedAt": 0, + "fetchFailureCount": 0, + "fetchFailureReason": null, + "fetchMeta": {}, + "isInvalidated": false, + "status": "success", + "fetchStatus": "fetching" + }, + "queryKey": [ + "jobs.isRunning", + { "library_id": "dc1c41b9-65c6-4a9d-a418-79e628b3ac59", "arg": null } + ], + "queryHash": "[\"jobs.isRunning\",{\"arg\":null,\"library_id\":\"dc1c41b9-65c6-4a9d-a418-79e628b3ac59\"}]" + }, + { + "state": { + "data": { "version": "0.1.0", "commit": "07401ac0" }, + "dataUpdateCount": 1, + "dataUpdatedAt": 1675912735057, + "error": null, + "errorUpdateCount": 0, + "errorUpdatedAt": 0, + "fetchFailureCount": 0, + "fetchFailureReason": null, + "fetchMeta": {}, + "isInvalidated": false, + "status": "success", + "fetchStatus": "fetching" + }, + "queryKey": ["buildInfo"], + "queryHash": "[\"buildInfo\"]" + }, + { + "state": { + "data": { + "version": "0.1.0", + "id": "afa03a65-861e-489e-918b-dac0252fe40c", + "name": "Oscars-MBP-2.lan", + "p2p_port": null, + "data_path": "/Users/oscar/Desktop/sd-ui-testing/sdserver_data" + }, + "dataUpdateCount": 1, + "dataUpdatedAt": 1675912735057, + "error": null, + "errorUpdateCount": 0, + "errorUpdatedAt": 0, + "fetchFailureCount": 0, + "fetchFailureReason": null, + "fetchMeta": {}, + "isInvalidated": false, + "status": "success", + "fetchStatus": "fetching" + }, + "queryKey": ["nodeState"], + "queryHash": "[\"nodeState\"]" + }, + { + "state": { + "data": { + "id": 1, + "date_captured": "2023-02-09T03:18:55.378+00:00", + "total_object_count": 0, + "library_db_size": "128", + "total_bytes_used": "0", + "total_bytes_capacity": "994662584320", + "total_unique_bytes": "0", + "total_bytes_free": "170520260608", + "preview_media_bytes": "0" + }, + "dataUpdateCount": 1, + "dataUpdatedAt": 1675912735381, + "error": null, + "errorUpdateCount": 0, + "errorUpdatedAt": 0, + "fetchFailureCount": 0, + "fetchFailureReason": null, + "fetchMeta": {}, + "isInvalidated": false, + "status": "success", + "fetchStatus": "fetching" + }, + "queryKey": [ + "library.getStatistics", + { "library_id": "dc1c41b9-65c6-4a9d-a418-79e628b3ac59", "arg": null } + ], + "queryHash": "[\"library.getStatistics\",{\"arg\":null,\"library_id\":\"dc1c41b9-65c6-4a9d-a418-79e628b3ac59\"}]" + } + ] +} diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index d965b4ff7..200fbbcae 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -2,6 +2,7 @@ import React, { Suspense } from 'react'; import ReactDOM from 'react-dom/client'; import '@sd/ui/style'; +import '~/patches'; // THIS MUST GO BEFORE importing the App import '~/patches'; import App from './App'; diff --git a/apps/web/tests/screenshots.test.ts b/apps/web/tests/screenshots.test.ts new file mode 100644 index 000000000..46e423b22 --- /dev/null +++ b/apps/web/tests/screenshots.test.ts @@ -0,0 +1,13 @@ +import { test } from '@playwright/test'; + +test('dark screenshot', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'dark' }); + await page.goto('/'); + await page.screenshot({ path: 'screenshots/overview-dark.png', fullPage: true }); +}); + +test('light screenshot', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'light' }); + await page.goto('/'); + await page.screenshot({ path: 'screenshots/overview-light.png', fullPage: true }); +}); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 3f7f1295b..e45eb13df 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -4,7 +4,7 @@ "rootDir": "src", "declarationDir": "dist" }, - "include": ["src"], + "include": ["src", "src/demoData.json"], "references": [ { "path": "../../packages/interface" diff --git a/packages/client/src/rspc.ts b/packages/client/src/rspc.ts index 54a2bba0a..e2450ed72 100644 --- a/packages/client/src/rspc.ts +++ b/packages/client/src/rspc.ts @@ -1,6 +1,5 @@ import { ProcedureDef } from '@rspc/client'; import { internal_createReactHooksFactory } from '@rspc/react'; -import { QueryClient } from '@tanstack/react-query'; import { LibraryArgs, Procedures } from './core'; import { currentLibraryCache } from './hooks'; import { normiCustomHooks } from './normi'; @@ -70,7 +69,6 @@ const libraryHooks = hooks.createHooks< } }); -export const queryClient = new QueryClient(); export const rspc = hooks.createHooks(); export const useBridgeQuery = nonLibraryHooks.useQuery; diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index 6da5b55f8..12f12ecfd 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -4,15 +4,15 @@ import { init } from '@sentry/browser'; import '@fontsource/inter/variable.css'; -import { QueryClientProvider, defaultContext } from '@tanstack/react-query'; +import { defaultContext, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import dayjs from 'dayjs'; import advancedFormat from 'dayjs/plugin/advancedFormat'; import duration from 'dayjs/plugin/duration'; import relativeTime from 'dayjs/plugin/relativeTime'; import { ErrorBoundary } from 'react-error-boundary'; -import { MemoryRouter } from 'react-router-dom'; -import { queryClient, useDebugState } from '@sd/client'; +import { BrowserRouter, MemoryRouter } from 'react-router-dom'; +import { useDebugState } from '@sd/client'; import { Dialogs } from '@sd/ui'; import { AppRouter } from './AppRouter'; import { ErrorFallback } from './ErrorFallback'; @@ -29,16 +29,15 @@ init({ integrations: [new HttpContextIntegration(), new DedupeIntegration()] }); -export default function SpacedriveInterface() { +export default function SpacedriveInterface({ router }: { router: 'memory' | 'browser' }) { + const Router = router === 'memory' ? MemoryRouter : BrowserRouter; return ( - - - - - - - + + + + + ); } diff --git a/packages/interface/src/components/layout/Sidebar.tsx b/packages/interface/src/components/layout/Sidebar.tsx index 6f40a27c0..83c3a02f7 100644 --- a/packages/interface/src/components/layout/Sidebar.tsx +++ b/packages/interface/src/components/layout/Sidebar.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query'; import clsx from 'clsx'; import { ArchiveBox, @@ -229,6 +230,7 @@ function IsRunningJob() { } function DebugPanel() { + const queryClient = useQueryClient(); const buildInfo = useBridgeQuery(['buildInfo']); const nodeState = useBridgeQuery(['nodeState']); const debugState = useDebugState(); @@ -289,6 +291,18 @@ function DebugPanel() { Enabled +
+ +
{/* {platform.showDevtools && ( Q)MIhI+;ZdIpp4h?{KgS1)3gz*GWN zQ79ogc|#z_a$c21 zm}^O%prWTh&MhN?aI$W4eo?BG0?2yB z#cX2j{?nMY`%hzzzcD>2oymUt+dP(OjMG1yWYVABJ(t^lxJwhlIeKsIpyqHFrGc^tg@O fvfF2F;WlDp=TcD6FUu*Mu6T!0dpq}T?jR2UAKbz3 delta 207 zcmX>*!QgJ2!3HPR>8G1nIHzyVW)_%!w3$U{@>33#%{y3Kn2^N1?lRd-_Fxj5d{%aL+n!Z=a(}2&vx82%gAU_%2OXw8S?%&On6}H$ zV2-`9{Z0|f+PujJ-6W^G)v}tmUwFs@#H>Kf2E^>!FFfS6iw@=^6ZNxU+ZyUETQ%>RZg=(zY+kfxp4)Op1zL-;u