Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/website/content/AGENTS.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ Render Spec has an experimental local React preview at https://threadplane.ai/do
State Management has an experimental local React preview at https://threadplane.ai/docs/render/guides/state-store?frontend=react. The React host owns copied immutable name, age and theme state; installed native RenderSpec resolves the three authored $state paths in read-only views. Edits survive partial playback, seek, Reset and sample selection. Invalid age drafts keep the last valid value, and reload restores the seed. Parser callbacks cannot replace host state; unknown bindings and executable fields are excluded. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-state-management-react` and verify with `npx nx e2e cockpit-render-state-management-react`.

Repeat Loops has an experimental local React preview at https://threadplane.ai/docs/render/guides/repeat-loops?frontend=react. The host owns immutable items with stable ids; native RenderSpec renders a repeated Card once and repeats its child rows keyed by item id. Add, remove and reverse items while preserving row identity and playback source. The list is bounded at 32; edits survive seek, Reset and sample changes, and reload restores the seed. Partial repeat metadata waits for its exact path and key. Read-only views exclude unknown expressions, nested repeats and executable fields. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-repeat-loops-react` and verify with `npx nx e2e cockpit-render-repeat-loops-react`.

Component Registry has an experimental local React preview at https://threadplane.ai/docs/render/guides/registry?frontend=react. The host supplies stable read-only component maps to installed native RenderSpec. Badge display chooses Registered, Omitted or an authored Fallback; fallback receives resolved props, element key and loading, while container views display native children exactly once. Choice survives partial playback, seek, Reset and sample selection without modifying JSON; reload restores Registered. Complete admission permits only four authored literal types. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-registry-react` and verify with `npx nx e2e cockpit-render-registry-react`.
2 changes: 2 additions & 0 deletions apps/website/content/CLAUDE.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ Render Spec has an experimental local React preview at https://threadplane.ai/do
State Management has an experimental local React preview at https://threadplane.ai/docs/render/guides/state-store?frontend=react. The React host owns copied immutable name, age and theme state; installed native RenderSpec resolves the three authored $state paths in read-only views. Edits survive partial playback, seek, Reset and sample selection. Invalid age drafts keep the last valid value, and reload restores the seed. Parser callbacks cannot replace host state; unknown bindings and executable fields are excluded. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-state-management-react` and verify with `npx nx e2e cockpit-render-state-management-react`.

Repeat Loops has an experimental local React preview at https://threadplane.ai/docs/render/guides/repeat-loops?frontend=react. The host owns immutable items with stable ids; native RenderSpec renders a repeated Card once and repeats its child rows keyed by item id. Add, remove and reverse items while preserving row identity and playback source. The list is bounded at 32; edits survive seek, Reset and sample changes, and reload restores the seed. Partial repeat metadata waits for its exact path and key. Read-only views exclude unknown expressions, nested repeats and executable fields. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-repeat-loops-react` and verify with `npx nx e2e cockpit-render-repeat-loops-react`.

Component Registry has an experimental local React preview at https://threadplane.ai/docs/render/guides/registry?frontend=react. The host supplies stable read-only component maps to installed native RenderSpec. Badge display chooses Registered, Omitted or an authored Fallback; fallback receives resolved props, element key and loading, while container views display native children exactly once. Choice survives partial playback, seek, Reset and sample selection without modifying JSON; reload restores Registered. Complete admission permits only four authored literal types. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-registry-react` and verify with `npx nx e2e cockpit-render-registry-react`.
227 changes: 227 additions & 0 deletions apps/website/e2e/react-registry-preview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { test, expect, type Page } from '@playwright/test';

const route = '/docs/render/guides/registry';
const iframe = (page: Page) =>
page.locator(
'iframe[title="Component Registry (React preview) live example"]'
);
const frame = (page: Page) => iframe(page).contentFrame();

test.beforeEach(async ({ page }) => {
const executions: string[] = [];
const errors: string[] = [];
page.on('request', (request) => {
if (
!['GET', 'HEAD'].includes(request.method()) &&
/\/agent(?:\/|$)|\/api\/native(?:\/|$)|\/api\/threads(?:\/|$)|\/threads(?:\/|$)/.test(
new URL(request.url()).pathname
)
)
executions.push(request.url());
});
page.on('pageerror', (error) => errors.push(error.message));
observers.set(page, { executions, errors });
});

const observers = new WeakMap<
Page,
{ executions: string[]; errors: string[] }
>();
test.afterEach(async ({ page }) => {
const observed = observers.get(page);
expect(observed?.executions).toEqual([]);
expect(observed?.errors).toEqual([]);
});

async function expectFreshRun(page: Page) {
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
'data-hydrated',
'true'
);
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
'data-workspace-mode',
'Run'
);
await expect(iframe(page)).toBeVisible();
await expect(page.getByLabel('Example UI')).toHaveValue('react');
await expect(page).toHaveURL(
(url) =>
url.pathname === route &&
url.searchParams.get('frontend') === 'react' &&
url.searchParams.get('mode') === 'run'
);
await expect(iframe(page)).toHaveAttribute(
'src',
/(?:localhost:4617(?:[/?#]|$)|\/render\/registry\/react\/?(?:[?#]|$))/
);
const example = frame(page);
await expect(
example.getByRole('combobox', { name: 'Badge display' })
).toHaveValue('registered');
await expect(example.getByRole('status')).toHaveText('Paused · 0 characters');
await expect(example.getByRole('combobox', { name: 'Sample' })).toHaveValue(
'0'
);
await expect(
example.getByRole('slider', { name: 'Playback position' })
).toHaveValue('0');
await expect(
example.getByRole('region', { name: 'Streaming JSON' }).locator('pre')
).toHaveText('');
await expect(
example.getByRole('region', { name: 'Render output' })
).toContainText('Play a sample to see its view.');
}

async function renderChangedSample(page: Page) {
const example = frame(page);
await example
.getByRole('combobox', { name: 'Badge display' })
.selectOption('fallback');
await example
.getByRole('combobox', { name: 'Sample' })
.selectOption({ label: 'Mixed Components' });
await example.getByRole('button', { name: 'Finish', exact: true }).click();
await expect(
example.getByRole('heading', { name: 'All Registered Types', exact: true })
).toBeVisible();
await expect(example.locator('[data-registry-fallback]')).toHaveCount(2);
await expect(example.locator('[data-registry-badge]')).toHaveCount(0);
await expect(
example.getByRole('region', { name: 'Streaming JSON' }).locator('pre')
).not.toHaveText('');
}

test('React Component Registry keeps local Docs, Code and Run aligned', async ({
page,
}, testInfo) => {
await page.goto(`${route}?frontend=react`, { waitUntil: 'domcontentloaded' });
await expect(page.getByLabel('Example UI')).toHaveValue('react');
await expect(
page.getByRole('heading', {
name: 'React Component Registry preview',
exact: true,
})
).toBeVisible();
await expect(
page.locator(
'[data-example-file="cockpit/render/registry/react/src/views.tsx"]'
)
).toBeVisible();
await expect(page.locator('[data-example-file*="/python/"]')).toHaveCount(0);
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
'data-hydrated',
'true'
);
const navigation = page.locator('[data-workspace-desktop-navigation]');
await navigation.getByRole('button', { name: 'Code', exact: true }).click();
await expect(page).toHaveURL(/frontend=react/);
await expect(page).toHaveURL(/mode=code/);
await navigation.getByRole('button', { name: /^Run(?:,|$)/ }).click();
await expectFreshRun(page);
await page.screenshot({
path: testInfo.outputPath('react-registry-desktop.png'),
});
});

test('React Component Registry restores fresh playback through frontend history', async ({
page,
}) => {
await page.goto(`${route}?frontend=react&mode=run`, {
waitUntil: 'domcontentloaded',
});
await expectFreshRun(page);
await renderChangedSample(page);
await page.getByLabel('Example UI').selectOption('angular');
await expect(page.getByLabel('Example UI')).toHaveValue('angular');
await expect(page).toHaveURL(`${route}?mode=run`);
await expect(page.locator('iframe')).toBeVisible();
await expect(page.locator('iframe')).toHaveAttribute(
'src',
/(?:localhost:4404(?:[/?#]|$)|\/render\/registry\/?(?:[?#]|$))/
);
await page.goBack({ waitUntil: 'domcontentloaded' });
await expectFreshRun(page);
});

test('React Component Registry reloads fresh playback after rendering another sample', async ({
page,
}) => {
await page.goto(`${route}?frontend=react&mode=run`, {
waitUntil: 'domcontentloaded',
});
await expectFreshRun(page);
await renderChangedSample(page);
await page.reload({ waitUntil: 'domcontentloaded' });
await expectFreshRun(page);
const example = frame(page);
await example.getByRole('button', { name: 'Finish', exact: true }).click();
await expect(example.locator('[data-registry-badge]')).toHaveCount(1);
await expect(example.locator('[data-registry-fallback]')).toHaveCount(0);
});

test('canonical Registry Run uses all native registry modes without execution or source changes', async ({
page,
}) => {
await page.goto(`${route}?frontend=react&mode=run`, {
waitUntil: 'domcontentloaded',
});
await expectFreshRun(page);
const frame = page.frameLocator('iframe'),
choice = frame.getByRole('combobox', { name: 'Badge display' });
const output = frame.getByRole('region', { name: 'Render output' });
const source = frame
.getByRole('region', { name: 'Streaming JSON' })
.locator('pre');
await expect(frame.getByRole('status')).toHaveText('Paused · 0 characters');
for (const [label, count] of [
['Basic Types', 1],
['Card Layout', 1],
['Mixed Components', 2],
] as const) {
await frame
.getByRole('combobox', { name: 'Sample' })
.selectOption({ label });
await choice.selectOption('registered');
await frame.getByRole('button', { name: 'Finish', exact: true }).click();
await expect(output.locator('[data-registry-badge]')).toHaveCount(count);
const original = await source.textContent();
await choice.selectOption('omitted');
await expect(output.locator('[data-registry-badge]')).toHaveCount(0);
await expect(output.locator('[data-registry-fallback]')).toHaveCount(0);
await choice.selectOption('fallback');
await expect(output.locator('[data-registry-fallback]')).toHaveCount(count);
await expect(source).toHaveText(original ?? '');
}
await frame.getByRole('button', { name: 'Reset', exact: true }).click();
await expect(choice).toHaveValue('fallback');
await frame.getByRole('button', { name: 'Finish', exact: true }).click();
await expect(output.locator('[data-registry-fallback]')).toHaveCount(2);
});

test('local React Registry remains usable on a narrow screen', async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(`${route}?frontend=react&mode=run`, {
waitUntil: 'domcontentloaded',
});
await expectFreshRun(page);
await expect(page.getByLabel('Example UI')).toBeVisible();
const frame = page.frameLocator('iframe');
await frame
.getByRole('combobox', { name: 'Badge display' })
.selectOption('fallback');
await frame.getByRole('button', { name: 'Finish', exact: true }).click();
await expect(frame.locator('[data-registry-fallback]')).toHaveText(
'Fallback: Registered'
);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth
)
).toBe(true);
await page.screenshot({
path: testInfo.outputPath('react-registry-mobile.png'),
});
});
7 changes: 7 additions & 0 deletions apps/website/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,13 @@ export const createWebsitePlaywrightConfig = (
reuseExistingServer,
timeout: 180_000,
},
{
command: 'node scripts/react-cockpit/serve.mjs render-registry --no-parent',
cwd: '../..',
url: 'http://127.0.0.1:4617',
reuseExistingServer,
timeout: 180_000,
},
]
: []),
]
Expand Down
3 changes: 2 additions & 1 deletion apps/website/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@
"cockpit-ag-ui-subagents-react",
"cockpit-render-spec-rendering-react",
"cockpit-render-state-management-react",
"cockpit-render-repeat-loops-react"
"cockpit-render-repeat-loops-react",
"cockpit-render-registry-react"
]
}
],
Expand Down
2 changes: 2 additions & 0 deletions apps/website/public/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ Render Spec has an experimental local React preview at https://threadplane.ai/do
State Management has an experimental local React preview at https://threadplane.ai/docs/render/guides/state-store?frontend=react. The React host owns copied immutable name, age and theme state; installed native RenderSpec resolves the three authored $state paths in read-only views. Edits survive partial playback, seek, Reset and sample selection. Invalid age drafts keep the last valid value, and reload restores the seed. Parser callbacks cannot replace host state; unknown bindings and executable fields are excluded. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-state-management-react` and verify with `npx nx e2e cockpit-render-state-management-react`.

Repeat Loops has an experimental local React preview at https://threadplane.ai/docs/render/guides/repeat-loops?frontend=react. The host owns immutable items with stable ids; native RenderSpec renders a repeated Card once and repeats its child rows keyed by item id. Add, remove and reverse items while preserving row identity and playback source. The list is bounded at 32; edits survive seek, Reset and sample changes, and reload restores the seed. Partial repeat metadata waits for its exact path and key. Read-only views exclude unknown expressions, nested repeats and executable fields. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-repeat-loops-react` and verify with `npx nx e2e cockpit-render-repeat-loops-react`.

Component Registry has an experimental local React preview at https://threadplane.ai/docs/render/guides/registry?frontend=react. The host supplies stable read-only component maps to installed native RenderSpec. Badge display chooses Registered, Omitted or an authored Fallback; fallback receives resolved props, element key and loading, while container views display native children exactly once. Choice survives partial playback, seek, Reset and sample selection without modifying JSON; reload restores Registered. Complete admission permits only four authored literal types. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-registry-react` and verify with `npx nx e2e cockpit-render-registry-react`.
2 changes: 2 additions & 0 deletions apps/website/public/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ Render Spec has an experimental local React preview at https://threadplane.ai/do
State Management has an experimental local React preview at https://threadplane.ai/docs/render/guides/state-store?frontend=react. The React host owns copied immutable name, age and theme state; installed native RenderSpec resolves the three authored $state paths in read-only views. Edits survive partial playback, seek, Reset and sample selection. Invalid age drafts keep the last valid value, and reload restores the seed. Parser callbacks cannot replace host state; unknown bindings and executable fields are excluded. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-state-management-react` and verify with `npx nx e2e cockpit-render-state-management-react`.

Repeat Loops has an experimental local React preview at https://threadplane.ai/docs/render/guides/repeat-loops?frontend=react. The host owns immutable items with stable ids; native RenderSpec renders a repeated Card once and repeats its child rows keyed by item id. Add, remove and reverse items while preserving row identity and playback source. The list is bounded at 32; edits survive seek, Reset and sample changes, and reload restores the seed. Partial repeat metadata waits for its exact path and key. Read-only views exclude unknown expressions, nested repeats and executable fields. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-repeat-loops-react` and verify with `npx nx e2e cockpit-render-repeat-loops-react`.

Component Registry has an experimental local React preview at https://threadplane.ai/docs/render/guides/registry?frontend=react. The host supplies stable read-only component maps to installed native RenderSpec. Badge display chooses Registered, Omitted or an authored Fallback; fallback receives resolved props, element key and loading, while container views display native children exactly once. Choice survives partial playback, seek, Reset and sample selection without modifying JSON; reload restores Registered. Complete admission permits only four authored literal types. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-registry-react` and verify with `npx nx e2e cockpit-render-registry-react`.
Loading
Loading