/**
* Invariant: saving a dev components panel hot reloads it. Keyed memory atoms survive the reload
* (unless their `version` changes), a "Reloaded" mark shows, and a save that does not load keeps
* the last good tree with an inline error naming file:line:column until the next save fixes it.
*/
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { disablePluginSystem, enableDevPlugins } from './helpers/plugin-smoke'
import { ISOLATED_FRAMES_E2E } from './helpers/plugin-frame-guest'
test.use(ISOLATED_FRAMES_E2E)
const TITLE = 'Hot Reload'
function panelSource(label: string, draftOptions = ''): string {
return [
"import { atom, Heading, Input, read, set, Stack, Text } from 'orca'",
'',
`const draft = atom('draft', ''${draftOptions})`,
'',
'export default function Panel() {',
' return (',
' ',
` Panel ${label}`,
' set(draft, v)} />',
" Draft: {read(draft) || '(empty)'}",
' ',
' )',
'}',
''
].join('\n')
}
async function openPanel(page: Page): Promise {
await page.evaluate(async () => {
const store = window.__store?.getState()
if (store && !store.rightSidebarOpen) {
store.toggleRightSidebar()
}
await window.api.plugins.refresh()
})
const button = page.getByRole('button', { name: TITLE })
await expect(button).toBeVisible({ timeout: 15_000 })
await button.click()
}
test('a dev components panel hot reloads with its memory atoms and survives a broken save', async ({
orcaPage
}) => {
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-components-hot-reload-'))
const pluginRoot = join(tempRoot, 'hot-reload')
const panelFile = join(pluginRoot, 'src', 'panel.tsx')
try {
await mkdir(join(pluginRoot, 'src'), { recursive: true })
await writeFile(
join(pluginRoot, 'orca-plugin.json'),
JSON.stringify({
manifestVersion: 2,
execution: 'sandboxed-ui',
id: 'hot-reload',
name: TITLE,
version: '1.0.0',
engines: { orca: '>=1.4.246' },
pluginApi: 1,
contributes: {
panels: [{ id: 'main', title: TITLE, entry: 'src/panel.tsx', ui: 'components' }]
}
})
)
await writeFile(panelFile, panelSource('v1'))
await enableDevPlugins(orcaPage, [pluginRoot])
await openPanel(orcaPage)
await expect(orcaPage.getByRole('heading', { name: 'Panel v1' })).toBeVisible({
timeout: 15_000
})
// Filling is idempotent, so a retry after the dev watcher's own first remount stays exact.
await expect(async () => {
await orcaPage.getByPlaceholder('Draft').fill('kept across saves')
await expect(orcaPage.getByText('Draft: kept across saves')).toBeVisible({ timeout: 2_000 })
}).toPass({ timeout: 15_000 })
await writeFile(panelFile, panelSource('v2'))
await expect(orcaPage.getByRole('heading', { name: 'Panel v2' })).toBeVisible({
timeout: 15_000
})
await expect(orcaPage.getByText('Draft: kept across saves')).toBeVisible()
await expect(orcaPage.getByText('Reloaded', { exact: true })).toBeVisible()
// A save that does not transpile: the v2 tree stays, with the error located in the source.
await writeFile(panelFile, "import { atom } from 'orca'\n\nconst label = 'broken'\nconst = 1\n")
const loadError = orcaPage.getByRole('alert').filter({ hasText: 'failed to load' })
await expect(loadError).toContainText('src/panel.tsx:4:7: Unexpected token', {
timeout: 15_000
})
await expect(orcaPage.getByRole('heading', { name: 'Panel v2' })).toBeVisible()
await expect(orcaPage.getByText('Draft: kept across saves')).toBeVisible()
// A throw is located at the source column (as `orca plugin check` reports), not the
// type-stripped one Chromium's stack names.
await writeFile(
panelFile,
"import { atom } from 'orca'\n\nconst n: number = Number('1'); if (n) throw new Error('boom')\n"
)
await expect(loadError).toContainText('src/panel.tsx:3:45: boom', { timeout: 15_000 })
await writeFile(panelFile, panelSource('v3'))
await expect(orcaPage.getByRole('heading', { name: 'Panel v3' })).toBeVisible({
timeout: 15_000
})
await expect(loadError).toHaveCount(0)
await expect(orcaPage.getByText('Draft: kept across saves')).toBeVisible()
// A new `version` means a new shape: that atom starts over instead of taking the old value.
await writeFile(panelFile, panelSource('v4', ', { version: 2 }'))
await expect(orcaPage.getByRole('heading', { name: 'Panel v4' })).toBeVisible({
timeout: 15_000
})
await expect(orcaPage.getByText('Draft: (empty)')).toBeVisible()
} finally {
await disablePluginSystem(orcaPage).catch(() => undefined)
await rm(tempRoot, { recursive: true, force: true })
}
})