Add docs and further stress tests to the demo
This commit is contained in:
@@ -6,12 +6,12 @@ import { useRoute } from 'vue-router'
|
||||
import PwaUpdate from './components/PwaUpdate.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const siblingRoutes = ['/inbox', '/stories', '/profile']
|
||||
const siblingRoutes = ['/inbox', '/stories', '/profile', '/profile/runtime-lab']
|
||||
const showTabs = computed(() => Boolean(route.meta.tab))
|
||||
const tabs: NativeTabItem[] = [
|
||||
{ label: 'Inbox', to: '/inbox', icon: '◉' },
|
||||
{ label: 'Stories', to: '/stories', icon: '◎' },
|
||||
{ label: 'You', to: '/profile', icon: '◇' },
|
||||
{ label: 'You', to: '/profile', icon: '◇', activeWhen: (current) => current.path.startsWith('/profile') },
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
16
apps/demo/src/components/AsyncLabData.vue
Normal file
16
apps/demo/src/components/AsyncLabData.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const requestedAt = Date.now()
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 1_000))
|
||||
const resolutionTime = Date.now() - requestedAt
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="lab-probe lab-probe--ready" data-testid="async-data-ready">
|
||||
<span class="lab-probe__icon" aria-hidden="true">✓</span>
|
||||
<div>
|
||||
<strong>Async payload available</strong>
|
||||
<p>Resolved {{ resolutionTime }} ms after this component mounted.</p>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
25
apps/demo/src/lab-state.ts
Normal file
25
apps/demo/src/lab-state.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { readonly, reactive } from 'vue'
|
||||
|
||||
const state = reactive({
|
||||
blockEntry: false,
|
||||
checks: 0,
|
||||
status: 'idle' as 'idle' | 'checking' | 'allowed' | 'blocked',
|
||||
})
|
||||
|
||||
export const runtimeLabGuard = readonly(state)
|
||||
|
||||
export function setRuntimeLabBlocked(blocked: boolean) {
|
||||
state.blockEntry = blocked
|
||||
if (state.status !== 'checking') state.status = 'idle'
|
||||
}
|
||||
|
||||
/** An intentionally asynchronous, stateful guard used by the stress demo. */
|
||||
export async function evaluateRuntimeLabEntry() {
|
||||
state.checks += 1
|
||||
state.status = 'checking'
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 320))
|
||||
const allowed = !state.blockEntry
|
||||
state.status = allowed ? 'allowed' : 'blocked'
|
||||
return allowed
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRouter, createWebHashHistory, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||
import { evaluateRuntimeLabEntry } from './lab-state'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{ path: '/', redirect: '/inbox' },
|
||||
@@ -14,6 +15,11 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/profile', name: 'profile', component: () => import('./views/ProfileView.vue'),
|
||||
meta: { tab: true, native: { siblingGroup: 'primary', siblingOrder: 2, siblingHistory: 'replace', gesture: 'full' } },
|
||||
},
|
||||
{
|
||||
path: '/profile/runtime-lab', name: 'runtime-lab', component: () => import('./views/RuntimeLabView.vue'),
|
||||
beforeEnter: evaluateRuntimeLabEntry,
|
||||
meta: { tab: true, native: { siblingGroup: 'primary', siblingOrder: 3, siblingHistory: 'push', presentation: 'slide', parent: '/profile', gesture: 'full' } },
|
||||
},
|
||||
{
|
||||
path: '/chat/:id', name: 'chat', component: () => import('./views/ChatView.vue'),
|
||||
meta: { native: { presentation: 'push', parent: '/inbox', gesture: 'edge' } },
|
||||
|
||||
@@ -241,6 +241,15 @@ html[data-pwa-edge-guard="active"] body {
|
||||
.lab-intro { display: flex; align-items: center; gap: 16px; margin: 20px 16px; padding: 18px; border: 1px solid rgba(124,92,255,.2); border-radius: 20px; background: rgba(124,92,255,.1); }
|
||||
.lab-intro > span { display: grid; width: 54px; height: 54px; place-items: center; border-radius: 17px; background: #7c5cff; font-size: 20px; font-weight: 800; }
|
||||
.lab-intro strong { font-size: 14px; }.lab-intro p { margin: 4px 0 0; color: var(--muted); font-size: 11px; }
|
||||
.lab-intro--timer > span { font-variant-numeric: tabular-nums; }
|
||||
.runtime-lab-screen .settings-group { padding-bottom: 0; }
|
||||
.lab-probe { display: flex; min-height: 86px; align-items: center; gap: 14px; padding: 15px; border-top: 1px solid var(--line); }
|
||||
.lab-probe__icon, .lab-spinner { display: grid; flex: 0 0 auto; width: 42px; height: 42px; place-items: center; border-radius: 14px; }
|
||||
.lab-probe__icon { color: #07130f; background: #3dd9aa; font-size: 20px; font-weight: 900; }
|
||||
.lab-probe strong { font-size: 13px; }
|
||||
.lab-probe p { margin: 4px 0 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
|
||||
.lab-spinner { border: 3px solid rgba(155,136,255,.2); border-top-color: #9b88ff; animation: lab-spin .7s linear infinite; }
|
||||
@keyframes lab-spin { to { transform: rotate(360deg); } }
|
||||
.settings-group { padding: 8px 0; }
|
||||
.settings-group h2 { margin: 8px 15px; color: #737986; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
|
||||
.settings-group > label, .settings-group > div { display: flex; min-height: 62px; align-items: center; justify-content: space-between; gap: 15px; padding: 10px 15px; border-top: 1px solid var(--line); }
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { NativeLink } from '@native-vue-router/core'
|
||||
import { NativeLink, useNativeRouter } from '@native-vue-router/core'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import { runtimeLabGuard, setRuntimeLabBlocked } from '../lab-state'
|
||||
|
||||
const native = useNativeRouter()
|
||||
|
||||
function toggleLabGuard() {
|
||||
setRuntimeLabBlocked(!runtimeLabGuard.blockEntry)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,6 +24,10 @@ import AppHeader from '../components/AppHeader.vue'
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-list">
|
||||
<a href="/profile/runtime-lab" @click.prevent="native.sibling('/profile/runtime-lab')"><span>⌁</span><strong>Runtime stress lab</strong><i>›</i></a>
|
||||
<button type="button" aria-label="Block Runtime Lab re-entry" :aria-pressed="runtimeLabGuard.blockEntry" @click="toggleLabGuard">
|
||||
<span>⌽</span><strong>Block cached lab re-entry</strong><i>{{ runtimeLabGuard.blockEntry ? 'On' : 'Off' }}</i>
|
||||
</button>
|
||||
<NativeLink to="/settings"><span>⚙︎</span><strong>Navigation lab</strong><i>›</i></NativeLink>
|
||||
<a href="https://github.com" target="_blank" rel="noreferrer"><span>⌘</span><strong>Project source</strong><i>↗</i></a>
|
||||
<button type="button"><span>◐</span><strong>Appearance</strong><i>System</i></button>
|
||||
|
||||
67
apps/demo/src/views/RuntimeLabView.vue
Normal file
67
apps/demo/src/views/RuntimeLabView.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import AsyncLabData from '../components/AsyncLabData.vue'
|
||||
import { runtimeLabGuard } from '../lab-state'
|
||||
|
||||
const mountedSeconds = ref(0)
|
||||
let mountedAt = 0
|
||||
let timer: number | undefined
|
||||
|
||||
onMounted(() => {
|
||||
mountedAt = Date.now()
|
||||
timer = window.setInterval(() => {
|
||||
mountedSeconds.value = Math.floor((Date.now() - mountedAt) / 1_000)
|
||||
}, 200)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer !== undefined) window.clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="screen screen--tabs runtime-lab-screen">
|
||||
<AppHeader title="Runtime stress lab" subtitle="A cached push-history sibling" back />
|
||||
|
||||
<section class="lab-intro lab-intro--timer">
|
||||
<span>{{ mountedSeconds }}</span>
|
||||
<div>
|
||||
<strong>Seconds mounted</strong>
|
||||
<p
|
||||
data-testid="mounted-seconds"
|
||||
:data-seconds="mountedSeconds"
|
||||
>This timer continues while the route is cached and out of view.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>Suspense boundary</h2>
|
||||
<Suspense :timeout="0">
|
||||
<AsyncLabData />
|
||||
<template #fallback>
|
||||
<article class="lab-probe lab-probe--loading" data-testid="async-data-loading" aria-live="polite">
|
||||
<span class="lab-spinner" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>Waiting for async payload</strong>
|
||||
<p>Data becomes available one second after the child mounts.</p>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</Suspense>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2>Guard and history state</h2>
|
||||
<div>
|
||||
<span><strong>Entry guard</strong><small>Asynchronous check #{{ runtimeLabGuard.checks }}</small></span>
|
||||
<b data-testid="lab-guard-status" :class="{ offline: runtimeLabGuard.status === 'blocked' }">{{ runtimeLabGuard.status }}</b>
|
||||
</div>
|
||||
<div>
|
||||
<span><strong>Sibling history</strong><small>This route pushes instead of replacing Profile</small></span>
|
||||
<b>push</b>
|
||||
</div>
|
||||
<p>Use Back to return to Profile. The route remains mounted in the native view cache, so its timer and resolved async component keep their state.</p>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user