@igstack/app-catalog-frontend-core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/__tests__/integration/harness/given.d.ts +5 -1
- package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +1 -1
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +1 -13
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/MarkdownText.d.ts +18 -0
- package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js +26 -0
- package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +1 -1
- package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +5 -2
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.d.ts +3 -1
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +17 -12
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +18 -4
- package/dist/esm/routeTree.gen.js +11 -4
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/routes/_layout/app.$slug.d.ts +3 -0
- package/dist/esm/routes/_layout/app._slug.js +21 -0
- package/dist/esm/routes/_layout/app._slug.js.map +1 -0
- package/dist/esm/routes/_layout/index.d.ts +0 -1
- package/dist/esm/routes/_layout/index.js +0 -1
- package/dist/esm/routes/_layout/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/integration/appDeepLink.integration.test.ts +55 -0
- package/src/__tests__/integration/harness/given.tsx +7 -3
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +1 -18
- package/src/modules/appCatalog/ui/components/MarkdownText.tsx +42 -0
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +10 -3
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +18 -15
- package/src/routeTree.gen.ts +21 -2
- package/src/routes/_layout/app.$slug.tsx +23 -0
- package/src/routes/_layout/index.tsx +0 -1
- package/dist/esm/__tests__/modules/gallery/EscapeDismissal.integration.test.d.ts +0 -0
- package/dist/esm/modules/appCatalog/ui/components/AppDetailModal.d.ts +0 -7
- package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +0 -63
- package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +0 -118
- package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +0 -365
- /package/dist/esm/__tests__/{modules/appCatalog/ScreenshotPreview.test.d.ts → integration/appDeepLink.integration.test.d.ts} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
2
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
2
3
|
import { X } from 'lucide-react'
|
|
3
4
|
import { useDeferredValue, useEffect, useMemo, useState } from 'react'
|
|
4
5
|
import { Button } from '~/ui/button'
|
|
@@ -13,25 +14,22 @@ import {
|
|
|
13
14
|
import { useAppCatalogContext } from '../../context/AppCatalogContext'
|
|
14
15
|
import { useAppClickHistory } from '../../hooks/useAppClickHistory'
|
|
15
16
|
import { useAppCounts } from '../../hooks/useAppCounts'
|
|
16
|
-
import { useUrlSyncedState } from '../../hooks/useUrlSyncedState'
|
|
17
17
|
import { searchResources } from '../../utils/searchApps'
|
|
18
18
|
import { OnboardingCard } from '../components/OnboardingCard'
|
|
19
19
|
import { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'
|
|
20
20
|
import { FilterBar } from '../filters/FilterBar'
|
|
21
21
|
import { AppCatalogGrid } from '../grid/AppCatalogGrid'
|
|
22
22
|
|
|
23
|
-
export function AppCatalogPage(
|
|
23
|
+
export function AppCatalogPage({
|
|
24
|
+
selectedSlug,
|
|
25
|
+
}: { selectedSlug?: string } = {}) {
|
|
24
26
|
const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()
|
|
25
27
|
const { state: filterState, actions } = useAppCatalogFilters()
|
|
26
28
|
const { getTopApps } = useAppClickHistory()
|
|
29
|
+
const navigate = useNavigate()
|
|
27
30
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
string | undefined
|
|
31
|
-
>({
|
|
32
|
-
key: 'app',
|
|
33
|
-
defaultValue: undefined,
|
|
34
|
-
})
|
|
31
|
+
// Selected app comes from the route path (/app/<slug>); undefined = nothing open
|
|
32
|
+
const selectedAppSlug = selectedSlug
|
|
35
33
|
|
|
36
34
|
// Search value from context (URL-synced in AppCatalogFiltersContext)
|
|
37
35
|
const searchValue = filterState.searchValue
|
|
@@ -108,19 +106,23 @@ export function AppCatalogPage() {
|
|
|
108
106
|
|
|
109
107
|
// Auto-open details when only 1 result
|
|
110
108
|
useEffect(() => {
|
|
111
|
-
if (filteredApps.length === 1 && filteredApps[0]) {
|
|
112
|
-
|
|
109
|
+
if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {
|
|
110
|
+
void navigate({
|
|
111
|
+
to: '/app/$slug',
|
|
112
|
+
params: { slug: filteredApps[0].slug },
|
|
113
|
+
replace: true,
|
|
114
|
+
})
|
|
113
115
|
}
|
|
114
|
-
}, [filteredApps,
|
|
116
|
+
}, [filteredApps, selectedAppSlug, navigate])
|
|
115
117
|
|
|
116
118
|
const handleAppClick = (app: Resource) => {
|
|
117
|
-
|
|
119
|
+
void navigate({ to: '/app/$slug', params: { slug: app.slug } })
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
const handleClearFilters = () => {
|
|
121
123
|
setSearchValue('')
|
|
122
124
|
actions.clearAllFilters()
|
|
123
|
-
|
|
125
|
+
void navigate({ to: '/' })
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// Calculate total apps count (respecting showDeprecated setting)
|
|
@@ -174,7 +176,7 @@ export function AppCatalogPage() {
|
|
|
174
176
|
variant="outline"
|
|
175
177
|
onClick={() => {
|
|
176
178
|
setSearchValue('')
|
|
177
|
-
|
|
179
|
+
void navigate({ to: '/' })
|
|
178
180
|
}}
|
|
179
181
|
className="gap-2"
|
|
180
182
|
>
|
|
@@ -190,6 +192,7 @@ export function AppCatalogPage() {
|
|
|
190
192
|
selectedAppSlug={selectedAppSlug}
|
|
191
193
|
groupingDefinition={groupingDefinition}
|
|
192
194
|
onAppClick={handleAppClick}
|
|
195
|
+
onClosePanel={() => void navigate({ to: '/' })}
|
|
193
196
|
hasSearch={!!deferredSearchValue}
|
|
194
197
|
searchQuery={deferredSearchValue}
|
|
195
198
|
totalAppsCount={totalAppsCount}
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Route as LayoutRouteImport } from './routes/_layout'
|
|
|
13
13
|
import { Route as LayoutIndexRouteImport } from './routes/_layout/index'
|
|
14
14
|
import { Route as AuthCallbackRouteImport } from './routes/auth.callback'
|
|
15
15
|
import { Route as LayoutLoginRouteImport } from './routes/_layout/login'
|
|
16
|
+
import { Route as LayoutAppSlugRouteImport } from './routes/_layout/app.$slug'
|
|
16
17
|
import { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index'
|
|
17
18
|
|
|
18
19
|
const LayoutRoute = LayoutRouteImport.update({
|
|
@@ -34,6 +35,11 @@ const LayoutLoginRoute = LayoutLoginRouteImport.update({
|
|
|
34
35
|
path: '/login',
|
|
35
36
|
getParentRoute: () => LayoutRoute,
|
|
36
37
|
} as any)
|
|
38
|
+
const LayoutAppSlugRoute = LayoutAppSlugRouteImport.update({
|
|
39
|
+
id: '/app/$slug',
|
|
40
|
+
path: '/app/$slug',
|
|
41
|
+
getParentRoute: () => LayoutRoute,
|
|
42
|
+
} as any)
|
|
37
43
|
const LayoutCatalogAppsIndexRoute = LayoutCatalogAppsIndexRouteImport.update({
|
|
38
44
|
id: '/catalog/apps/',
|
|
39
45
|
path: '/catalog/apps/',
|
|
@@ -44,12 +50,14 @@ export interface FileRoutesByFullPath {
|
|
|
44
50
|
'/': typeof LayoutIndexRoute
|
|
45
51
|
'/login': typeof LayoutLoginRoute
|
|
46
52
|
'/auth/callback': typeof AuthCallbackRoute
|
|
53
|
+
'/app/$slug': typeof LayoutAppSlugRoute
|
|
47
54
|
'/catalog/apps/': typeof LayoutCatalogAppsIndexRoute
|
|
48
55
|
}
|
|
49
56
|
export interface FileRoutesByTo {
|
|
50
57
|
'/login': typeof LayoutLoginRoute
|
|
51
58
|
'/auth/callback': typeof AuthCallbackRoute
|
|
52
59
|
'/': typeof LayoutIndexRoute
|
|
60
|
+
'/app/$slug': typeof LayoutAppSlugRoute
|
|
53
61
|
'/catalog/apps': typeof LayoutCatalogAppsIndexRoute
|
|
54
62
|
}
|
|
55
63
|
export interface FileRoutesById {
|
|
@@ -58,19 +66,21 @@ export interface FileRoutesById {
|
|
|
58
66
|
'/_layout/login': typeof LayoutLoginRoute
|
|
59
67
|
'/auth/callback': typeof AuthCallbackRoute
|
|
60
68
|
'/_layout/': typeof LayoutIndexRoute
|
|
69
|
+
'/_layout/app/$slug': typeof LayoutAppSlugRoute
|
|
61
70
|
'/_layout/catalog/apps/': typeof LayoutCatalogAppsIndexRoute
|
|
62
71
|
}
|
|
63
72
|
export interface FileRouteTypes {
|
|
64
73
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
65
|
-
fullPaths: '/' | '/login' | '/auth/callback' | '/catalog/apps/'
|
|
74
|
+
fullPaths: '/' | '/login' | '/auth/callback' | '/app/$slug' | '/catalog/apps/'
|
|
66
75
|
fileRoutesByTo: FileRoutesByTo
|
|
67
|
-
to: '/login' | '/auth/callback' | '/' | '/catalog/apps'
|
|
76
|
+
to: '/login' | '/auth/callback' | '/' | '/app/$slug' | '/catalog/apps'
|
|
68
77
|
id:
|
|
69
78
|
| '__root__'
|
|
70
79
|
| '/_layout'
|
|
71
80
|
| '/_layout/login'
|
|
72
81
|
| '/auth/callback'
|
|
73
82
|
| '/_layout/'
|
|
83
|
+
| '/_layout/app/$slug'
|
|
74
84
|
| '/_layout/catalog/apps/'
|
|
75
85
|
fileRoutesById: FileRoutesById
|
|
76
86
|
}
|
|
@@ -109,6 +119,13 @@ declare module '@tanstack/react-router' {
|
|
|
109
119
|
preLoaderRoute: typeof LayoutLoginRouteImport
|
|
110
120
|
parentRoute: typeof LayoutRoute
|
|
111
121
|
}
|
|
122
|
+
'/_layout/app/$slug': {
|
|
123
|
+
id: '/_layout/app/$slug'
|
|
124
|
+
path: '/app/$slug'
|
|
125
|
+
fullPath: '/app/$slug'
|
|
126
|
+
preLoaderRoute: typeof LayoutAppSlugRouteImport
|
|
127
|
+
parentRoute: typeof LayoutRoute
|
|
128
|
+
}
|
|
112
129
|
'/_layout/catalog/apps/': {
|
|
113
130
|
id: '/_layout/catalog/apps/'
|
|
114
131
|
path: '/catalog/apps'
|
|
@@ -122,12 +139,14 @@ declare module '@tanstack/react-router' {
|
|
|
122
139
|
interface LayoutRouteChildren {
|
|
123
140
|
LayoutLoginRoute: typeof LayoutLoginRoute
|
|
124
141
|
LayoutIndexRoute: typeof LayoutIndexRoute
|
|
142
|
+
LayoutAppSlugRoute: typeof LayoutAppSlugRoute
|
|
125
143
|
LayoutCatalogAppsIndexRoute: typeof LayoutCatalogAppsIndexRoute
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
const LayoutRouteChildren: LayoutRouteChildren = {
|
|
129
147
|
LayoutLoginRoute: LayoutLoginRoute,
|
|
130
148
|
LayoutIndexRoute: LayoutIndexRoute,
|
|
149
|
+
LayoutAppSlugRoute: LayoutAppSlugRoute,
|
|
131
150
|
LayoutCatalogAppsIndexRoute: LayoutCatalogAppsIndexRoute,
|
|
132
151
|
}
|
|
133
152
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'
|
|
3
|
+
import { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'
|
|
4
|
+
import { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_layout/app/$slug')({
|
|
7
|
+
component: RouteComponent,
|
|
8
|
+
async loader() {
|
|
9
|
+
const appCatalogLoader = await appCatalogRouteLoader()
|
|
10
|
+
return { appCatalogLoader }
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function RouteComponent() {
|
|
15
|
+
const { queryClient, trpcClient } = Route.useRouteContext()
|
|
16
|
+
const { slug } = Route.useParams()
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>
|
|
20
|
+
<AppCatalogPage selectedSlug={slug} />
|
|
21
|
+
</AppCatalogLayout>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -5,7 +5,6 @@ import { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayou
|
|
|
5
5
|
import { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'
|
|
6
6
|
|
|
7
7
|
const searchSchema = z.object({
|
|
8
|
-
app: z.string().optional(),
|
|
9
8
|
filterTag: z.string().optional(),
|
|
10
9
|
})
|
|
11
10
|
|
|
File without changes
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { Resource } from '@igstack/app-catalog-backend-core';
|
|
2
|
-
export interface AppDetailModalProps {
|
|
3
|
-
app: Resource;
|
|
4
|
-
isOpen: boolean;
|
|
5
|
-
onClose: () => void;
|
|
6
|
-
}
|
|
7
|
-
export declare function AppDetailModal({ app, isOpen, onClose }: AppDetailModalProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import '@testing-library/jest-dom/vitest'
|
|
4
|
-
|
|
5
|
-
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
6
|
-
import { AppDetailModal } from '~/modules/appCatalog/ui/components/AppDetailModal'
|
|
7
|
-
import { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'
|
|
8
|
-
import type { AppCatalogContextIface } from '~/modules/appCatalog/context/AppCatalogContext'
|
|
9
|
-
|
|
10
|
-
// Minimal context value — ScreenshotPreview doesn't use context directly,
|
|
11
|
-
// but AccessSection (sibling in AppDetailModal) does.
|
|
12
|
-
const minimalContext: AppCatalogContextIface = {
|
|
13
|
-
resources: [],
|
|
14
|
-
isLoadingApps: false,
|
|
15
|
-
tagsDefinitions: [],
|
|
16
|
-
approvalMethods: [],
|
|
17
|
-
persons: [],
|
|
18
|
-
groups: [],
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function renderWithContext(app: Resource) {
|
|
22
|
-
return render(
|
|
23
|
-
<AppCatalogContext value={minimalContext}>
|
|
24
|
-
<AppDetailModal app={app} isOpen={true} onClose={() => {}} />
|
|
25
|
-
</AppCatalogContext>,
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function makeApp(screenshotIds: string[]): Resource {
|
|
30
|
-
return {
|
|
31
|
-
id: 'app-1',
|
|
32
|
-
slug: 'test-app',
|
|
33
|
-
displayName: 'Test App',
|
|
34
|
-
screenshotIds,
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Task 2.2 — unit test for thumbnail size parameter
|
|
39
|
-
// Requirements: 1.1, 1.3
|
|
40
|
-
describe('ScreenshotPreview thumbnail URLs', () => {
|
|
41
|
-
it('appends ?size=600 to each thumbnail src', () => {
|
|
42
|
-
const ids = ['abc123', 'def456', 'ghi789']
|
|
43
|
-
renderWithContext(makeApp(ids))
|
|
44
|
-
|
|
45
|
-
const imgs = screen.getAllByRole('img', { name: /screenshot/i })
|
|
46
|
-
for (const [i, id] of ids.entries()) {
|
|
47
|
-
expect(imgs[i]).toHaveAttribute('src', `/api/screenshots/${id}?size=600`)
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('renders one thumbnail per screenshot ID', () => {
|
|
52
|
-
const ids = ['id-1', 'id-2', 'id-3']
|
|
53
|
-
renderWithContext(makeApp(ids))
|
|
54
|
-
|
|
55
|
-
const imgs = screen.getAllByRole('img', { name: /screenshot/i })
|
|
56
|
-
expect(imgs).toHaveLength(ids.length)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('shows placeholder when screenshotIds is empty', () => {
|
|
60
|
-
renderWithContext(makeApp([]))
|
|
61
|
-
expect(screen.getByText(/no screenshots available/i)).toBeInTheDocument()
|
|
62
|
-
})
|
|
63
|
-
})
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import '@testing-library/jest-dom/vitest'
|
|
4
|
-
|
|
5
|
-
import type { Resource } from '@igstack/app-catalog-backend-core'
|
|
6
|
-
import { AppDetailModal } from '~/modules/appCatalog/ui/components/AppDetailModal'
|
|
7
|
-
import { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'
|
|
8
|
-
import type { AppCatalogContextIface } from '~/modules/appCatalog/context/AppCatalogContext'
|
|
9
|
-
|
|
10
|
-
// Task 3.3 — integration test for full Escape dismissal chain
|
|
11
|
-
// Requirements: 6.7
|
|
12
|
-
// Chain: Fullscreen_View → Gallery_Dialog → App_Card
|
|
13
|
-
// Each Escape press dismisses only the innermost active layer.
|
|
14
|
-
|
|
15
|
-
const minimalContext: AppCatalogContextIface = {
|
|
16
|
-
resources: [],
|
|
17
|
-
isLoadingApps: false,
|
|
18
|
-
tagsDefinitions: [],
|
|
19
|
-
approvalMethods: [],
|
|
20
|
-
persons: [],
|
|
21
|
-
groups: [],
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function makeApp(): Resource {
|
|
25
|
-
return {
|
|
26
|
-
id: 'app-1',
|
|
27
|
-
slug: 'test-app',
|
|
28
|
-
displayName: 'Test App',
|
|
29
|
-
screenshotIds: ['screenshot-1', 'screenshot-2'],
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function pressEscape() {
|
|
34
|
-
fireEvent.keyDown(document, {
|
|
35
|
-
key: 'Escape',
|
|
36
|
-
bubbles: true,
|
|
37
|
-
cancelable: true,
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe('Escape key — layered dismissal chain (integration)', () => {
|
|
42
|
-
it('Escape dismisses layers one at a time: Fullscreen → Gallery → App Card', async () => {
|
|
43
|
-
const onClose = vi.fn()
|
|
44
|
-
const app = makeApp()
|
|
45
|
-
|
|
46
|
-
render(
|
|
47
|
-
<AppCatalogContext value={minimalContext}>
|
|
48
|
-
<AppDetailModal app={app} isOpen={true} onClose={onClose} />
|
|
49
|
-
</AppCatalogContext>,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
// App card is open — modal content should be visible
|
|
53
|
-
expect(screen.getByText('Test App')).toBeInTheDocument()
|
|
54
|
-
|
|
55
|
-
// Step 1: Open the gallery by clicking a thumbnail
|
|
56
|
-
const thumbnail = screen.getAllByRole('button', {
|
|
57
|
-
name: /screenshot 1/i,
|
|
58
|
-
})[0]
|
|
59
|
-
await act(async () => {
|
|
60
|
-
fireEvent.click(thumbnail!)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
// Gallery dialog should now be open (Radix renders into a portal)
|
|
64
|
-
await waitFor(() => {
|
|
65
|
-
expect(document.querySelector('[role="dialog"]')).toBeInTheDocument()
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// Step 2: Enter fullscreen by clicking the first active gallery image
|
|
69
|
-
// Gallery images have alt "Test App screenshot" (without index number)
|
|
70
|
-
const galleryImgs = document.querySelectorAll(
|
|
71
|
-
'[role="dialog"] img[alt="Test App screenshot"]',
|
|
72
|
-
)
|
|
73
|
-
expect(galleryImgs.length).toBeGreaterThan(0)
|
|
74
|
-
await act(async () => {
|
|
75
|
-
fireEvent.click(galleryImgs[0]!)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// Fullscreen overlay should be visible
|
|
79
|
-
await waitFor(() => {
|
|
80
|
-
expect(
|
|
81
|
-
document.querySelector('[aria-label="Fullscreen view"]'),
|
|
82
|
-
).toBeInTheDocument()
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
// --- First Escape: exits fullscreen, gallery stays open, app card stays open ---
|
|
86
|
-
await act(async () => {
|
|
87
|
-
pressEscape()
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
await waitFor(() => {
|
|
91
|
-
expect(
|
|
92
|
-
document.querySelector('[aria-label="Fullscreen view"]'),
|
|
93
|
-
).not.toBeInTheDocument()
|
|
94
|
-
})
|
|
95
|
-
// Gallery dialog still present
|
|
96
|
-
expect(document.querySelector('[role="dialog"]')).toBeInTheDocument()
|
|
97
|
-
// App card still open (onClose not called)
|
|
98
|
-
expect(onClose).not.toHaveBeenCalled()
|
|
99
|
-
|
|
100
|
-
// --- Second Escape: closes gallery dialog, app card stays open ---
|
|
101
|
-
await act(async () => {
|
|
102
|
-
pressEscape()
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
await waitFor(() => {
|
|
106
|
-
expect(document.querySelector('[role="dialog"]')).not.toBeInTheDocument()
|
|
107
|
-
})
|
|
108
|
-
// App card still open (onClose not called yet)
|
|
109
|
-
expect(onClose).not.toHaveBeenCalled()
|
|
110
|
-
|
|
111
|
-
// --- Third Escape: closes app card ---
|
|
112
|
-
await act(async () => {
|
|
113
|
-
pressEscape()
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
expect(onClose).toHaveBeenCalledTimes(1)
|
|
117
|
-
})
|
|
118
|
-
})
|