diff --git a/playwright.config.ts b/playwright.config.ts index 834b8e6..7298da8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -30,6 +30,7 @@ if (isCI || includeWebKit) { export default defineConfig({ testDir: 'playwright', + fullyParallel: isCI, timeout: isCI ? 120_000 : 20_000, retries: isCI ? 1 : 0, workers: isCI ? 1 : undefined, diff --git a/playwright/github-byot-ai.spec.ts b/playwright/github-byot-ai.spec.ts index ca9e24a..c473920 100644 --- a/playwright/github-byot-ai.spec.ts +++ b/playwright/github-byot-ai.spec.ts @@ -23,6 +23,11 @@ test('PR/BYOT controls are visible and chat stays hidden until token connect', a exact: true, includeHidden: true, }) + const workspacesToggle = page.getByRole('button', { + name: 'Workspaces', + exact: true, + includeHidden: true, + }) await expect(byotControls).toBeVisible() await expect(page.getByRole('textbox', { name: 'GitHub token' })).toBeVisible() await expect(page.getByRole('button', { name: 'Add GitHub token' })).toBeVisible() @@ -30,6 +35,8 @@ test('PR/BYOT controls are visible and chat stays hidden until token connect', a await expect(page.getByRole('heading', { name: 'AI Chat' })).toBeHidden() await expect(prToggle).toHaveCount(1) await expect(prToggle).toBeHidden() + await expect(workspacesToggle).toHaveCount(1) + await expect(workspacesToggle).toBeHidden() }) test('chat becomes available after token connect', async ({ page }) => { @@ -37,6 +44,7 @@ test('chat becomes available after token connect', async ({ page }) => { await connectByotWithSingleRepo(page) await expect(page.getByRole('button', { name: 'Open pull request' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Workspaces' })).toBeVisible() await expect(page.getByRole('button', { name: 'Chat' })).toBeVisible() }) @@ -49,12 +57,19 @@ test('BYOT controls render with default app entry', async ({ page }) => { exact: true, includeHidden: true, }) + const workspacesToggle = page.getByRole('button', { + name: 'Workspaces', + exact: true, + includeHidden: true, + }) await expect(byotControls).toBeVisible() await expect(page.getByRole('textbox', { name: 'GitHub token' })).toBeVisible() await expect(page.getByRole('button', { name: 'Add GitHub token' })).toBeVisible() await expect(page.getByRole('button', { name: 'Chat' })).toBeHidden() await expect(prToggle).toHaveCount(1) await expect(prToggle).toBeHidden() + await expect(workspacesToggle).toHaveCount(1) + await expect(workspacesToggle).toBeHidden() }) test('GitHub token info panel reflects missing and present token states', async ({ diff --git a/playwright/helpers/app-test-helpers.ts b/playwright/helpers/app-test-helpers.ts index 9224765..c8763a5 100644 --- a/playwright/helpers/app-test-helpers.ts +++ b/playwright/helpers/app-test-helpers.ts @@ -171,6 +171,22 @@ export const openWorkspaceTab = async (page: Page, fileName: string) => { await page.getByRole('button', { name: pattern }).click() } +const replaceEditorSource = async ({ + editorContent, + source, +}: { + editorContent: ReturnType + source: string +}) => { + for (let attempt = 0; attempt < 2; attempt += 1) { + await editorContent.fill('') + await editorContent.fill(source) + await editorContent.press('End') + await editorContent.type(' ') + await editorContent.press('Backspace') + } +} + export const reorderWorkspaceTabBefore = async ( page: Page, { from, to }: { from: string; to: string }, @@ -199,35 +215,29 @@ export const setWorkspaceTabSource = async ( }, ) => { await openWorkspaceTab(page, fileName) + await expect(page.getByRole('region', { name: fileName })).toBeVisible() const editorContent = page .locator(`.editor-panel[data-editor-kind="${kind}"] .cm-content`) .first() - await editorContent.fill(source) - await editorContent.press('End') - await editorContent.type(' ') - await editorContent.press('Backspace') + await replaceEditorSource({ editorContent, source }) } export const setComponentEditorSource = async (page: Page, source: string) => { await page.getByRole('button', { name: 'Open tab App.tsx' }).click() + await expect(page.getByRole('region', { name: 'App.tsx' })).toBeVisible() const editorContent = page .locator('.editor-panel[data-editor-kind="component"] .cm-content') .first() - await editorContent.fill(source) - await editorContent.press('End') - await editorContent.type(' ') - await editorContent.press('Backspace') + await replaceEditorSource({ editorContent, source }) } export const setStylesEditorSource = async (page: Page, source: string) => { await page.getByRole('button', { name: 'Open tab app.css' }).click() + await expect(page.getByRole('region', { name: 'app.css' })).toBeVisible() const editorContent = page .locator('.editor-panel[data-editor-kind="styles"] .cm-content') .first() - await editorContent.fill(source) - await editorContent.press('End') - await editorContent.type(' ') - await editorContent.press('Backspace') + await replaceEditorSource({ editorContent, source }) } export const getActiveComponentEditorLineNumber = async (page: Page) => { diff --git a/playwright/rendering-modes.spec.ts b/playwright/rendering-modes.spec.ts index 2335e4d..b51aa85 100644 --- a/playwright/rendering-modes.spec.ts +++ b/playwright/rendering-modes.spec.ts @@ -577,7 +577,7 @@ test('shows App-only error when auto render is disabled and App is missing', asy ) }) -test('auto render implicitly wraps source with App in dom and react modes', async ({ +test('auto render shows App-only error in dom and react modes when App is missing', async ({ page, }) => { await waitForInitialRender(page) @@ -589,9 +589,9 @@ test('auto render implicitly wraps source with App in dom and react modes', asyn 'const Button = () => ', ) - await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') - await expect(getPreviewFrame(page).getByRole('button')).toContainText( - 'implicit app dom', + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') + await expect(page.locator('#preview-host pre')).toContainText( + 'Expected a function or const named App.', ) await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react') @@ -604,13 +604,13 @@ test('auto render implicitly wraps source with App in dom and react modes', asyn page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(), ).toContainText('implicit app react') - await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') - await expect(getPreviewFrame(page).getByRole('button')).toContainText( - 'implicit app react', + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') + await expect(page.locator('#preview-host pre')).toContainText( + 'Expected a function or const named App.', ) }) -test('auto render implicit App includes multiple component declarations', async ({ +test('auto render renders successfully when explicit App is defined in dom and react modes', async ({ page, }) => { await waitForInitialRender(page) @@ -619,15 +619,25 @@ test('auto render implicit App includes multiple component declarations', async await setComponentEditorSource( page, - [ - 'const OtherButton = () => ', - 'const Button = () => ', - ].join('\n'), + 'const App = () => ', ) await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') - await expect(getPreviewFrame(page).getByRole('button')).toHaveCount(2) - await expect(getPreviewFrame(page).getByRole('button')).toContainText(['bar', 'foo']) + await expect(getPreviewFrame(page).getByRole('button')).toContainText( + 'explicit app dom', + ) + + await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react') + await expect(page.getByRole('combobox', { name: 'Render mode' })).toHaveValue('react') + await setComponentEditorSource( + page, + 'const App = () => ', + ) + + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') + await expect(getPreviewFrame(page).getByRole('button')).toContainText( + 'explicit app react', + ) }) test('auto render does not treat lowercase helpers as implicit components', async ({ @@ -651,7 +661,7 @@ test('auto render does not treat lowercase helpers as implicit components', asyn ) }) -test('auto render wraps standalone JSX with trailing semicolon and comment', async ({ +test('auto render shows App-only error for standalone JSX expression', async ({ page, }) => { await waitForInitialRender(page) @@ -663,13 +673,13 @@ test('auto render wraps standalone JSX with trailing semicolon and comment', asy '() as any; // trailing', ) - await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') - await expect(getPreviewFrame(page).getByRole('button')).toContainText( - 'implicit app from jsx expression', + await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') + await expect(page.locator('#preview-host pre')).toContainText( + 'Expected a function or const named App.', ) }) -test('auto render requires explicit App for declarations plus top-level JSX expression', async ({ +test('auto render shows App-only error for declarations plus top-level JSX expression', async ({ page, }) => { await waitForInitialRender(page) @@ -687,7 +697,7 @@ test('auto render requires explicit App for declarations plus top-level JSX expr await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error') await expect(page.locator('#preview-host pre')).toContainText( - 'Top-level JSX with declarations or imports requires an explicit App component.', + 'Expected a function or const named App.', ) }) diff --git a/src/index.html b/src/index.html index d625d9a..f3c62eb 100644 --- a/src/index.html +++ b/src/index.html @@ -165,6 +165,7 @@

aria-controls="workspaces-drawer" title="Manage local workspaces" disabled + hidden >