Skip to content

Commit df185a3

Browse files
feat: tab activation guards, startup re-entry restore order, specifier compatibility resolution.
1 parent cfb915b commit df185a3

4 files changed

Lines changed: 307 additions & 31 deletions

File tree

playwright/rendering-modes.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ import {
1313
waitForInitialRender,
1414
} from './helpers/app-test-helpers.js'
1515

16+
const renameWorkspaceTab = async (
17+
page: import('@playwright/test').Page,
18+
{
19+
from,
20+
to,
21+
}: {
22+
from: string
23+
to: string
24+
},
25+
) => {
26+
await page.getByRole('button', { name: `Rename tab ${from}` }).click()
27+
const renameInput = page.getByLabel(`Rename ${from}`)
28+
await renameInput.fill(to)
29+
await renameInput.press('Enter')
30+
}
31+
1632
test.beforeEach(async ({ page }) => {
1733
await resetWorkbenchStorage(page)
1834
})
@@ -626,6 +642,79 @@ test('workspace tabs resolve extensionless relative imports through virtual modu
626642
)
627643
})
628644

645+
test('workspace tabs resolve .js specifiers to tsx workspace modules when exact match is missing', async ({
646+
page,
647+
}) => {
648+
await waitForInitialRender(page)
649+
650+
await ensurePanelToolsVisible(page, 'component')
651+
652+
await addWorkspaceTab(page)
653+
654+
await setWorkspaceTabSource(page, {
655+
fileName: 'module.tsx',
656+
source: "export const label = 'js specifier to tsx fallback'",
657+
})
658+
659+
await setWorkspaceTabSource(page, {
660+
fileName: 'App.tsx',
661+
source: [
662+
"import { label } from './module.js'",
663+
'export const App = () => <button type="button">{label}</button>',
664+
].join('\n'),
665+
})
666+
667+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
668+
await expect(getPreviewFrame(page).getByRole('button')).toContainText(
669+
'js specifier to tsx fallback',
670+
)
671+
})
672+
673+
test('workspace graph errors are deterministic for ambiguous extension compatibility matches', async ({
674+
page,
675+
}) => {
676+
await waitForInitialRender(page)
677+
678+
await ensurePanelToolsVisible(page, 'component')
679+
680+
await addWorkspaceTab(page)
681+
await addWorkspaceTab(page)
682+
683+
await renameWorkspaceTab(page, {
684+
from: 'module-2.tsx',
685+
to: 'module.ts',
686+
})
687+
688+
await setWorkspaceTabSource(page, {
689+
fileName: 'module.tsx',
690+
source: "export const label = 'from tsx'",
691+
})
692+
693+
await setWorkspaceTabSource(page, {
694+
fileName: 'module.ts',
695+
source: "export const label = 'from ts'",
696+
})
697+
698+
await setWorkspaceTabSource(page, {
699+
fileName: 'App.tsx',
700+
source: [
701+
"import { label } from './module.js'",
702+
'export const App = () => <button type="button">{label}</button>',
703+
].join('\n'),
704+
})
705+
706+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
707+
await expect(page.locator('#preview-host pre')).toContainText(
708+
'Preview entry references ambiguous workspace module: ./module.js',
709+
)
710+
await expect(page.locator('#preview-host pre')).toContainText(
711+
'src/components/module.ts',
712+
)
713+
await expect(page.locator('#preview-host pre')).toContainText(
714+
'src/components/module.tsx',
715+
)
716+
})
717+
629718
test('workspace graph errors for missing modules remain deterministic', async ({
630719
page,
631720
}) => {

playwright/workspace-tabs.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,62 @@ test('renaming module tab preserves source content', async ({ page }) => {
115115
.first()
116116
await expect(editorContent).toContainText('export const Value = () => <p>Kept</p>')
117117
})
118+
119+
test('active tab remains source of truth for visible editor panel', async ({ page }) => {
120+
await waitForInitialRender(page)
121+
122+
await addWorkspaceTab(page)
123+
await addWorkspaceTab(page)
124+
125+
const componentPanel = page.locator('#editor-panel-component')
126+
const stylesPanel = page.locator('#editor-panel-styles')
127+
128+
await page.getByRole('tab', { name: 'Open tab app.css' }).click()
129+
await expect(page.getByRole('tab', { name: 'Open tab app.css' })).toHaveAttribute(
130+
'aria-selected',
131+
'true',
132+
)
133+
await expect(stylesPanel).not.toHaveAttribute('hidden', '')
134+
await expect(componentPanel).toHaveAttribute('hidden', '')
135+
136+
await page.getByRole('tab', { name: 'Open tab module-2.tsx' }).click()
137+
await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveAttribute(
138+
'aria-selected',
139+
'true',
140+
)
141+
await expect(componentPanel).not.toHaveAttribute('hidden', '')
142+
await expect(stylesPanel).toHaveAttribute('hidden', '')
143+
144+
await page.locator('#collapse-component').click()
145+
await page.getByRole('tab', { name: 'Open tab app.css' }).click()
146+
147+
await expect(page.getByRole('tab', { name: 'Open tab app.css' })).toHaveAttribute(
148+
'aria-selected',
149+
'true',
150+
)
151+
await expect(stylesPanel).not.toHaveAttribute('hidden', '')
152+
await expect(componentPanel).toHaveAttribute('hidden', '')
153+
})
154+
155+
test('startup restores last active workspace tab after reload', async ({ page }) => {
156+
await waitForInitialRender(page)
157+
158+
await addWorkspaceTab(page)
159+
await addWorkspaceTab(page)
160+
161+
await page.getByRole('tab', { name: 'Open tab module-2.tsx' }).click()
162+
await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveAttribute(
163+
'aria-selected',
164+
'true',
165+
)
166+
167+
await page.reload()
168+
await waitForInitialRender(page)
169+
170+
await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveAttribute(
171+
'aria-selected',
172+
'true',
173+
)
174+
await expect(page.locator('#editor-panel-component')).not.toHaveAttribute('hidden', '')
175+
await expect(page.locator('#editor-panel-styles')).toHaveAttribute('hidden', '')
176+
})

src/app.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ let workspaceSaver = null
154154
let activeWorkspaceRecordId = ''
155155
let activeWorkspaceCreatedAt = null
156156
let isApplyingWorkspaceSnapshot = false
157+
let hasCompletedInitialWorkspaceBootstrap = false
157158
const workspaceTabsState = createWorkspaceTabsState({
158159
tabs: [
159160
{
@@ -1093,6 +1094,21 @@ const ensureWorkspaceTabsShape = tabs => {
10931094
})
10941095
}
10951096

1097+
const resolveWorkspaceActiveTabId = ({ tabs, requestedActiveTabId }) => {
1098+
const nextTabs = Array.isArray(tabs) ? tabs : []
1099+
const requestedId = toNonEmptyWorkspaceText(requestedActiveTabId)
1100+
1101+
if (requestedId && nextTabs.some(tab => tab?.id === requestedId)) {
1102+
return requestedId
1103+
}
1104+
1105+
if (nextTabs.some(tab => tab?.id === 'component')) {
1106+
return 'component'
1107+
}
1108+
1109+
return toNonEmptyWorkspaceText(nextTabs[0]?.id)
1110+
}
1111+
10961112
const buildWorkspaceTabsSnapshot = () => {
10971113
const activeTabId = workspaceTabsState.getActiveTabId()
10981114
return workspaceTabsState.getTabs().map(tab => {
@@ -1265,7 +1281,10 @@ const applyWorkspaceRecord = async (workspace, { silent = false } = {}) => {
12651281

12661282
workspaceTabsState.replaceTabs({
12671283
tabs: nextTabs,
1268-
activeTabId: workspace.activeTabId,
1284+
activeTabId: resolveWorkspaceActiveTabId({
1285+
tabs: nextTabs,
1286+
requestedActiveTabId: workspace.activeTabId,
1287+
}),
12691288
})
12701289

12711290
const nextRenderMode = normalizeRenderMode(workspace.renderMode)
@@ -1292,7 +1311,9 @@ const applyWorkspaceRecord = async (workspace, { silent = false } = {}) => {
12921311

12931312
renderWorkspaceTabs()
12941313

1295-
maybeRender()
1314+
if (hasCompletedInitialWorkspaceBootstrap) {
1315+
maybeRender()
1316+
}
12961317
await refreshLocalContextOptions()
12971318
if (!silent) {
12981319
setStatus('Loaded local workspace context.', 'neutral')
@@ -1345,13 +1366,15 @@ const setActiveWorkspaceTab = tabId => {
13451366
return
13461367
}
13471368

1369+
const currentActiveTabId = workspaceTabsState.getActiveTabId()
13481370
const targetTab = workspaceTabsState.getTab(normalizedTabId)
13491371
if (!targetTab) {
1350-
const fallbackTab = getActiveWorkspaceTab()
1351-
if (fallbackTab) {
1352-
loadWorkspaceTabIntoEditor(fallbackTab)
1353-
renderWorkspaceTabs()
1354-
}
1372+
return
1373+
}
1374+
1375+
if (targetTab.id === currentActiveTabId) {
1376+
loadWorkspaceTabIntoEditor(targetTab)
1377+
renderWorkspaceTabs()
13551378
return
13561379
}
13571380

@@ -2973,10 +2996,12 @@ setTypeDiagnosticsDetails({ headline: '' })
29732996
renderRuntime.setStyleCompiling(false)
29742997
setCdnLoading(true)
29752998
initializePreviewBackgroundPicker()
2976-
void loadPreferredWorkspaceContext().catch(() => {
2999+
const workspaceRestoreReady = loadPreferredWorkspaceContext().catch(() => {
29773000
setStatus('Could not restore local workspace context.', 'neutral')
29783001
})
29793002
void initializeCodeEditors().then(async () => {
3003+
await workspaceRestoreReady
3004+
29803005
const activeTab = getActiveWorkspaceTab()
29813006
if (activeTab) {
29823007
setActiveWorkspaceTab(activeTab.id)
@@ -2988,5 +3013,6 @@ void initializeCodeEditors().then(async () => {
29883013
setCssSource(stylesTab.content)
29893014
}
29903015

3016+
hasCompletedInitialWorkspaceBootstrap = true
29913017
await renderPreview()
29923018
})

0 commit comments

Comments
 (0)