Skip to content
Open
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
54 changes: 54 additions & 0 deletions e2e/tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect, test } from '@playwright/test';

// Runs under the `chromium` project (already authenticated via the saved
// storage state). The /tabs demo page is a 2-tab layout whose SECOND tab owns
// a text_input. Committing that input reruns the script server-side, which
// patches the whole tree back into the client. This spec pins the behaviour
// that selection survives that patch: the active tab is uncontrolled Radix
// state on a stable-keyed component, so a rerun triggered from within tab 2
// must leave the user on tab 2 — not snap them back to tab 1.

test.describe('tabs survive a rerun', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tabs');
await expect(page.getByRole('heading', { name: /^Tabs$/ })).toBeVisible({
timeout: 10_000,
});
});

test('a rerun ordered from inside tab 2 keeps tab 2 active', async ({
page,
}) => {
const firstTab = page.getByRole('tab', { name: 'First tab' });
const secondTab = page.getByRole('tab', { name: 'Second tab' });

// First tab is the default selection.
await expect(firstTab).toHaveAttribute('data-state', 'active');
await expect(secondTab).toHaveAttribute('data-state', 'inactive');

// Switch to tab 2 and confirm its content (incl. the input) mounted.
await secondTab.click();
await expect(secondTab).toHaveAttribute('data-state', 'active');
const nameInput = page.getByLabel('Your name');
await expect(nameInput).toBeVisible();
await expect(page.getByText('Hello, stranger!')).toBeVisible();

// Commit a value from inside tab 2 — this is what orders the rerun.
// text_input commits on blur/Enter, not per keystroke.
await nameInput.fill('Ada');
await nameInput.press('Enter');

// The echo updates only after the script reruns and re-renders, so seeing
// it proves the rerun landed.
await expect(page.getByText('Hello, Ada!')).toBeVisible({
timeout: 10_000,
});

// The whole point: the rerun must not have bounced us back to tab 1.
await expect(secondTab).toHaveAttribute('data-state', 'active');
await expect(firstTab).toHaveAttribute('data-state', 'inactive');
// Radix unmounts inactive tab content, so the input staying visible is an
// independent confirmation that tab 2 is still the one on screen.
await expect(nameInput).toBeVisible();
});
});
2 changes: 2 additions & 0 deletions examples/demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ run(
sb.link({ label: '📈 Stats', href: '/stats' });
sb.link({ label: '📐 Columns', href: '/columns' });
sb.link({ label: '🎛️ Widgets', href: '/widgets' });
sb.link({ label: '🗂️ Tabs', href: '/tabs' });
sb.link({ label: '📁 File Upload', href: '/file-upload' });
sb.write({ body: '---' });
sb.write({ body: `📍 \`${currentPath}\`` });
Expand All @@ -54,6 +55,7 @@ run(
pages.columns(br.page({ path: '/columns' }));
pages.charts(br.page({ path: '/charts' }));
pages.widgets(br.page({ path: '/widgets' }));
pages.tabs(br.page({ path: '/tabs' }));

// const br = brBase.base({});
br.write({ body: `# Backroad LLM Example\n---` });
Expand Down
2 changes: 2 additions & 0 deletions examples/demo/src/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { backroadLLMExample } from './llm';
import { backroadMarkdownExample } from './markdown';
import { backroadSelectExample } from './select';
import { backroadStatsExample } from './stats';
import { backroadTabsExample } from './tabs';
import { backroadWidgetsExample } from './widgets';

export const pages = {
Expand All @@ -20,4 +21,5 @@ export const pages = {
fileUpload: backroadFileUploadExample,
iframe: backroadIframeExample,
widgets: backroadWidgetsExample,
tabs: backroadTabsExample,
};
25 changes: 25 additions & 0 deletions examples/demo/src/pages/tabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BackroadNodeManager } from '@backroad/backroad';

// A 2-tab layout whose SECOND tab owns a text_input. Changing that input
// commits a value and reruns the script. The point of the demo (and its e2e
// spec) is that the rerun must NOT yank the user back to the first tab: tab
// selection is uncontrolled Radix state living on a stable-keyed component, so
// it has to survive the tree patch. Each tab echoes its own state so the spec
// can prove the rerun actually happened while the active tab held steady.
export const backroadTabsExample = (br: BackroadNodeManager) => {
br.write({ body: '# Tabs' });

const [first, second] = br.tabs({ labels: ['First tab', 'Second tab'] });

first.write({ body: 'This is the first tab — it has no interactive state.' });

second.write({ body: 'This is the second tab.' });
const name = second.textInput({
label: 'Your name',
placeholder: 'Type here, then press Enter',
defaultValue: '',
});
// Echo updates only after the input commits and the script reruns, so the
// spec asserting this text also asserts that a rerun occurred.
second.write({ body: `Hello, ${name || 'stranger'}!` });
};
Loading