fix: resolve node demo launch policy before opening its player
This commit is contained in:
@@ -55,6 +55,27 @@ describe('useAppLauncherStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['close', 'another-app', 'none'])('waits for node demo launch policy, cancellation: %s', async cancel => {
|
||||
const id = `node-demo-delayed-${cancel}`
|
||||
const previousFetch = globalThis.fetch
|
||||
let release!: (value: unknown) => void
|
||||
vi.stubGlobal('fetch', vi.fn(() => new Promise(resolve => { release = resolve })))
|
||||
const launcher = useAppLauncherStore()
|
||||
launcher.openSession(id)
|
||||
expect(launcher.panelAppId).toBeNull()
|
||||
if (cancel === 'close') launcher.closePanel()
|
||||
if (cancel === 'another-app') launcher.openSession('mempool')
|
||||
release({ ok: true, json: async () => ({ scope: 'single-node-demo', expires_at: new Date(Date.now() + 60000).toISOString(), apps: {
|
||||
[id]: { version: '1', manifest: { app: { metadata: { launch: { media_controls: 'archipelago-v1' } } } } },
|
||||
} }) })
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
expect(launcher.panelAppId).toBe(cancel === 'none' ? id : cancel === 'another-app' ? 'mempool' : null)
|
||||
launcher.openSession(id)
|
||||
expect(launcher.panelAppId).toBe(id)
|
||||
expect(launcher.mediaAppId).toBe(id)
|
||||
vi.stubGlobal('fetch', previousFetch)
|
||||
})
|
||||
|
||||
it('blocks both browser and embedded launch while HTTP is unready', () => {
|
||||
const app = useAppStore()
|
||||
app.data = { 'package-data': { gitea: { state: 'running', 'ui-ready': false, health: 'healthy', manifest: { id: 'gitea', title: 'Gitea' } } } } as never
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { appHasMediaBridge } from '@/views/discover/curatedApps'
|
||||
import { appHasMediaBridge, ensureNodeAppAvailable, nodeAppIsAvailable } from '@/views/discover/curatedApps'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
@@ -239,6 +239,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
const mediaAppId = ref<string | null>(null)
|
||||
/** Optional deep-link path inside the active app (e.g. /tx/<hash> for mempool) */
|
||||
const panelPath = ref<string | null>(null)
|
||||
let launchGeneration = 0
|
||||
|
||||
function openSessionNow(appId: string, opts: LaunchOptions = {}) {
|
||||
const pkg = useAppStore().data?.['package-data']?.[appId]
|
||||
@@ -299,6 +300,19 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
* Previously each Apps view owned a private modal, so Home skipped the
|
||||
* Portainer first-run token entirely. */
|
||||
function openSession(appId: string, opts: LaunchOptions = {}) {
|
||||
const generation = ++launchGeneration
|
||||
if (appId.startsWith('node-demo-') && !nodeAppIsAvailable(appId)) {
|
||||
useToast().info('Loading this node’s demo app…')
|
||||
void ensureNodeAppAvailable(appId).then(available => {
|
||||
if (generation !== launchGeneration) return
|
||||
if (!available) {
|
||||
useToast().error('This node’s demo catalog is unavailable. Please retry.')
|
||||
return
|
||||
}
|
||||
openSession(appId, opts)
|
||||
})
|
||||
return
|
||||
}
|
||||
// Home/goal/deep-link launchers do not pass through AppCard.canLaunch.
|
||||
// Apply the same readiness gate here so a container that has just entered
|
||||
// `running` cannot race nginx and show a transient 502 to the user.
|
||||
@@ -392,6 +406,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
launchGeneration += 1
|
||||
panelAppId.value = null
|
||||
panelPath.value = null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user