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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ Never:
- Edit generated output folders unless explicitly requested.
- Modify node_modules or lockfiles unless explicitly requested.
- Reintroduce cross-workspace overwrite/delete behavior with any changes.
- Use eslint disable comments.
9 changes: 6 additions & 3 deletions playwright/github-pr-drawer/active-context-sync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getWorkspaceTabsRecord,
mockRepositoryBranches,
openMostRecentStoredWorkspaceContext,
openStoredWorkspaceContextByHead,
renameWorkspaceTab,
seedActivePrWorkspaceContext,
seedLocalWorkspaceContexts,
Expand Down Expand Up @@ -1304,7 +1305,6 @@ test('Active PR context push commit uses Git Database API atomic path by default
await connectByotWithSingleRepo(page)
await openMostRecentStoredWorkspaceContext(page)
await ensureOpenPrDrawerOpen(page)

await setComponentEditorSource(page, 'const commitMarker = 2')
await setStylesEditorSource(page, '.commit-marker { color: blue; }')
const pushCommitMessage = 'chore: push active context sync (atomic)'
Expand Down Expand Up @@ -1563,8 +1563,11 @@ test('Open PR uses module tab paths when stale target file paths collide', async
},
])

await connectByotWithSingleRepo(page)
await openMostRecentStoredWorkspaceContext(page)
await connectByotWithSingleRepo(page, {
autoOpenWorkspace: false,
assertPrRepositorySelected: false,
})
await openStoredWorkspaceContextByHead(page, 'develop/open-pr-stale-target-paths')
await ensureOpenPrDrawerOpen(page)

const commitMessage = 'chore: open pr with stale module target path metadata'
Expand Down
76 changes: 56 additions & 20 deletions playwright/helpers/app-test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { Page } from '@playwright/test'

const webServerMode = process.env.PLAYWRIGHT_WEB_SERVER_MODE ?? 'dev'
const pagesWithStubbedExternalFonts = new WeakSet<Page>()

export const appEntryPath =
webServerMode === 'preview' ? '/index.html' : '/src/index.html'
Expand Down Expand Up @@ -71,10 +72,37 @@
}
}

const stubExternalFontRequests = async (page: Page) => {
if (pagesWithStubbedExternalFonts.has(page)) {
return
}

pagesWithStubbedExternalFonts.add(page)

await page.route('https://fonts.googleapis.com/**', async route => {
await route.fulfill({
status: 200,
contentType: 'text/css; charset=utf-8',
body: '',
headers: {
'cache-control': 'public, max-age=31536000, immutable',
},
})
})

await page.route('https://fonts.gstatic.com/**', async route => {
await route.fulfill({
status: 204,
body: '',
})
})
}

export const waitForAppReady = async (page: Page, path = appEntryPath) => {
await stubExternalFontRequests(page)
await navigateToApp(page, path)
await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible()
await expect

Check failure on line 105 in playwright/helpers/app-test-helpers.ts

View workflow job for this annotation

GitHub Actions / E2E (Playwright, webkit, shard 3/4)

[webkit] › playwright/rendering-modes/core.spec.ts:1033:1 › react mode mounts into internal non-div host to avoid div selector bleed

2) [webkit] › playwright/rendering-modes/core.spec.ts:1033:1 › react mode mounts into internal non-div host to avoid div selector bleed Error: expect(received).toBe(expected) // Object.is equality Expected: true Received: false Call Log: - Timeout 90000ms exceeded while waiting on the predicate at helpers/app-test-helpers.ts:105 103 | await navigateToApp(page, path) 104 | await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible() > 105 | await expect | ^ 106 | .poll(async () => { 107 | const statusText = ( 108 | await page.getByRole('status', { name: 'App status' }).textContent() at waitForAppReady (/home/runner/work/develop/develop/playwright/helpers/app-test-helpers.ts:105:3) at waitForInitialRender (/home/runner/work/develop/develop/playwright/helpers/app-test-helpers.ts:169:3) at /home/runner/work/develop/develop/playwright/rendering-modes/core.spec.ts:1036:3

Check failure on line 105 in playwright/helpers/app-test-helpers.ts

View workflow job for this annotation

GitHub Actions / E2E (Playwright, webkit, shard 3/4)

[webkit] › playwright/github-pr-drawer/open-pr-create.spec.ts:1422:1 › Blank-slate startup persists inactive local workspace before PAT

1) [webkit] › playwright/github-pr-drawer/open-pr-create.spec.ts:1422:1 › Blank-slate startup persists inactive local workspace before PAT Error: expect(received).toBe(expected) // Object.is equality Expected: true Received: false Call Log: - Timeout 90000ms exceeded while waiting on the predicate at helpers/app-test-helpers.ts:105 103 | await navigateToApp(page, path) 104 | await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible() > 105 | await expect | ^ 106 | .poll(async () => { 107 | const statusText = ( 108 | await page.getByRole('status', { name: 'App status' }).textContent() at waitForAppReady (/home/runner/work/develop/develop/playwright/helpers/app-test-helpers.ts:105:3) at /home/runner/work/develop/develop/playwright/github-pr-drawer/open-pr-create.spec.ts:1427:3
.poll(async () => {
const statusText = (
await page.getByRole('status', { name: 'App status' }).textContent()
Expand Down Expand Up @@ -483,8 +511,12 @@
page: Page,
{
branchesByRepo,
autoOpenWorkspace = true,
assertPrRepositorySelected = true,
}: {
branchesByRepo?: BranchesByRepo
autoOpenWorkspace?: boolean
assertPrRepositorySelected?: boolean
} = {},
) => {
await page.route('https://api.github.com/user/repos**', async route => {
Expand Down Expand Up @@ -525,33 +557,37 @@
await workspacesRepositoryFilter.selectOption('knightedcodemonkey/develop')
await expect(workspacesRepositoryFilter).toHaveValue('knightedcodemonkey/develop')

const initializeButton = page.getByRole('button', {
name: 'Initialize',
exact: true,
})
if (autoOpenWorkspace) {
const initializeButton = page.getByRole('button', {
name: 'Initialize',
exact: true,
})

if (await initializeButton.isVisible()) {
await initializeButton.click()
} else {
const storedWorkspace = page.getByLabel('Stored workspace')
if (await storedWorkspace.isVisible()) {
const workspaceValue = await storedWorkspace
.locator('option:not([value=""])')
.first()
.getAttribute('value')

if (workspaceValue) {
await storedWorkspace.selectOption(workspaceValue)
await page.getByRole('button', { name: 'Open', exact: true }).click()
if (await initializeButton.isVisible()) {
await initializeButton.click()
} else {
const storedWorkspace = page.getByLabel('Stored workspace')
if (await storedWorkspace.isVisible()) {
const workspaceValue = await storedWorkspace
.locator('option:not([value=""])')
.first()
.getAttribute('value')

if (workspaceValue) {
await storedWorkspace.selectOption(workspaceValue)

Check failure on line 577 in playwright/helpers/app-test-helpers.ts

View workflow job for this annotation

GitHub Actions / E2E (Playwright, webkit, shard 2/4)

[webkit] › playwright/github-pr-drawer/open-pr-confirmation.spec.ts:213:1 › Open PR drawer strips App wrapper from committed source when toggled off

1) [webkit] › playwright/github-pr-drawer/open-pr-confirmation.spec.ts:213:1 › Open PR drawer strips App wrapper from committed source when toggled off Error: locator.selectOption: Test timeout of 120000ms exceeded. Call log: - waiting for getByLabel('Stored workspace') - locator resolved to <select id="workspaces-select" aria-label="Stored workspace">…</select> - attempting select option action 2 × waiting for element to be visible and enabled - element is not visible - retrying select option action - waiting 20ms 2 × waiting for element to be visible and enabled - element is not visible - retrying select option action - waiting 100ms 229 × waiting for element to be visible and enabled - element is not visible - retrying select option action - waiting 500ms at helpers/app-test-helpers.ts:577 575 | 576 | if (workspaceValue) { > 577 | await storedWorkspace.selectOption(workspaceValue) | ^ 578 | await page.getByRole('button', { name: 'Open', exact: true }).click() 579 | } 580 | } at connectByotWithSingleRepo (/home/runner/work/develop/develop/playwright/helpers/app-test-helpers.ts:577:33) at /home/runner/work/develop/develop/playwright/github-pr-drawer/open-pr-confirmation.spec.ts:338:3

Check failure on line 577 in playwright/helpers/app-test-helpers.ts

View workflow job for this annotation

GitHub Actions / E2E (Playwright, webkit, shard 1/4)

[webkit] › playwright/github-byot-ai.spec.ts:926:1 › AI chat can disable editor context payload via checkbox

2) [webkit] › playwright/github-byot-ai.spec.ts:926:1 › AI chat can disable editor context payload via checkbox Error: locator.selectOption: Test timeout of 120000ms exceeded. Call log: - waiting for getByLabel('Stored workspace') - locator resolved to <select id="workspaces-select" aria-label="Stored workspace">…</select> - attempting select option action 2 × waiting for element to be visible and enabled - element is not visible - retrying select option action - waiting 20ms 2 × waiting for element to be visible and enabled - element is not visible - retrying select option action - waiting 100ms 228 × waiting for element to be visible and enabled - element is not visible - retrying select option action - waiting 500ms at helpers/app-test-helpers.ts:577 575 | 576 | if (workspaceValue) { > 577 | await storedWorkspace.selectOption(workspaceValue) | ^ 578 | await page.getByRole('button', { name: 'Open', exact: true }).click() 579 | } 580 | } at connectByotWithSingleRepo (/home/runner/work/develop/develop/playwright/helpers/app-test-helpers.ts:577:33) at /home/runner/work/develop/develop/playwright/github-byot-ai.spec.ts:945:3
await page.getByRole('button', { name: 'Open', exact: true }).click()
}
}
}
}

await ensureWorkspacesDrawerClosed(page)

const repoSelect = page.getByLabel('Pull request repository')
await expect(repoSelect).toHaveValue('knightedcodemonkey/develop')
await expect(repoSelect).toBeDisabled()
if (assertPrRepositorySelected) {
const repoSelect = page.getByLabel('Pull request repository')
await expect(repoSelect).toHaveValue('knightedcodemonkey/develop')

Check failure on line 588 in playwright/helpers/app-test-helpers.ts

View workflow job for this annotation

GitHub Actions / E2E (Playwright, chromium)

[chromium] › playwright/github-byot-ai.spec.ts:1269:1 › AI chat renders a single apply action for multiple targets resolving to the same tab

1) [chromium] › playwright/github-byot-ai.spec.ts:1269:1 › AI chat renders a single apply action for multiple targets resolving to the same tab Error: expect(locator).toHaveValue(expected) failed Locator: getByLabel('Pull request repository') Expected: "knightedcodemonkey/develop" Received: "" Timeout: 90000ms Call log: - Expect "toHaveValue" with timeout 90000ms - waiting for getByLabel('Pull request repository') 93 × locator resolved to <select disabled id="github-pr-repo-select" aria-label="Pull request repository">…</select> - unexpected value "" at helpers/app-test-helpers.ts:588 586 | if (assertPrRepositorySelected) { 587 | const repoSelect = page.getByLabel('Pull request repository') > 588 | await expect(repoSelect).toHaveValue('knightedcodemonkey/develop') | ^ 589 | await expect(repoSelect).toBeDisabled() 590 | } 591 | at connectByotWithSingleRepo (/home/runner/work/develop/develop/playwright/helpers/app-test-helpers.ts:588:30) at /home/runner/work/develop/develop/playwright/github-byot-ai.spec.ts:1325:3

Check failure on line 588 in playwright/helpers/app-test-helpers.ts

View workflow job for this annotation

GitHub Actions / E2E (Playwright, webkit, shard 1/4)

[webkit] › playwright/github-byot-ai.spec.ts:308:1 › workspace context status stays visible without PAT and after PAT connect

1) [webkit] › playwright/github-byot-ai.spec.ts:308:1 › workspace context status stays visible without PAT and after PAT connect Error: expect(locator).toHaveValue(expected) failed Locator: getByLabel('Pull request repository') Expected: "knightedcodemonkey/develop" Received: "" Timeout: 90000ms Call log: - Expect "toHaveValue" with timeout 90000ms - waiting for getByLabel('Pull request repository') 93 × locator resolved to <select disabled id="github-pr-repo-select" aria-label="Pull request repository">…</select> - unexpected value "" at helpers/app-test-helpers.ts:588 586 | if (assertPrRepositorySelected) { 587 | const repoSelect = page.getByLabel('Pull request repository') > 588 | await expect(repoSelect).toHaveValue('knightedcodemonkey/develop') | ^ 589 | await expect(repoSelect).toBeDisabled() 590 | } 591 | at connectByotWithSingleRepo (/home/runner/work/develop/develop/playwright/helpers/app-test-helpers.ts:588:30) at /home/runner/work/develop/develop/playwright/github-byot-ai.spec.ts:317:3
await expect(repoSelect).toBeDisabled()
}

await expect(
page.getByRole('button', {
Expand Down
47 changes: 47 additions & 0 deletions playwright/rendering-modes/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const readLatestWorkspaceSnapshot = async (page: import('@playwright/test').Page

return {
renderMode: typeof latest.renderMode === 'string' ? latest.renderMode : '',
fontCssUrl: typeof latest.fontCssUrl === 'string' ? latest.fontCssUrl : '',
styleLanguage:
typeof primaryStylesTab?.language === 'string' ? primaryStylesTab.language : '',
}
Expand All @@ -147,6 +148,12 @@ const readPreviewUserStyleText = async (page: import('@playwright/test').Page) =
})
}

const readPreviewBodyFontFamily = async (page: import('@playwright/test').Page) => {
return getPreviewFrame(page)
.locator('html')
.evaluate(() => getComputedStyle(document.body).fontFamily || '')
}

test.beforeEach(async ({ page }) => {
await resetWorkbenchStorage(page)
})
Expand Down Expand Up @@ -228,6 +235,46 @@ test('reactJsx tag interpolation renders memo and forwardRef components', async
.toBe(0)
})

test('workspace font CSS URL applies to preview and persists per workspace', async ({
page,
}) => {
await waitForInitialRender(page)

await page.getByRole('button', { name: 'Workspaces' }).click()

const fontCssUrlInput = page.getByRole('textbox', {
name: 'Workspace font stylesheet URL',
})
await fontCssUrlInput.fill(
'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&display=swap',
)
await page.getByRole('button', { name: 'Load', exact: true }).click()

await expect
.poll(async () => (await readPreviewBodyFontFamily(page)).toLowerCase())
.toContain('ibm plex sans')

Comment thread
knightedcodemonkey marked this conversation as resolved.
await expect
.poll(async () => {
const snapshot = await readLatestWorkspaceSnapshot(page)
return snapshot?.fontCssUrl ?? ''
})
.toBe(
'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&display=swap',
)

await page.reload()
await waitForInitialRender(page)
await page.getByRole('button', { name: 'Workspaces' }).click()

await expect(fontCssUrlInput).toHaveValue(
'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&display=swap',
)
await expect
.poll(async () => (await readPreviewBodyFontFamily(page)).toLowerCase())
.toContain('ibm plex sans')
})

test('react mode keeps App.ts entry but surfaces rename guidance until compatible', async ({
page,
}) => {
Expand Down
36 changes: 36 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ import { createGitHubPrDrawer } from './modules/github/pr/drawer/controller/crea
import { createLayoutThemeController } from './modules/ui/layout-theme.js'
import { createLintDiagnosticsController } from './modules/diagnostics/lint-diagnostics.js'
import { createPreviewBackgroundController } from './modules/preview/preview-background.js'
import {
createPreviewFontController,
defaultPreviewFontCssUrl,
normalizePreviewFontCssUrl,
} from './modules/preview/preview-font.js'
import { getReactEntryTabCompatibilityError } from './modules/preview/preview-entry-resolver.js'
import { createRenderRuntimeController } from './modules/preview/render-runtime.js'
import { createTypeDiagnosticsController } from './modules/diagnostics/type-diagnostics.js'
Expand All @@ -74,6 +79,8 @@ import { ensureJsxTransformSource } from './modules/preview/jsx-transform-runtim
import { createEditorPoolManager } from './modules/editor/editor-pool-manager.js'
import { createWorkspaceTabsState } from './modules/workspace/workspace-tabs-state.js'
import { createWorkspacesDrawer } from './modules/workspace/workspaces-drawer/drawer.js'
import { createApplyWorkspaceFontCssUrl } from './modules/app-core/workspace-font-css-url-load.js'
import { createPreviewFontSetup } from './modules/app-core/preview-font-setup.js'
import {
createDebouncedWorkspaceSaver,
createWorkspaceStorageAdapter,
Expand Down Expand Up @@ -151,6 +158,8 @@ const workspacesInitialize = document.getElementById('workspaces-initialize')
const workspacesShare = document.getElementById('workspaces-share')
const workspacesNew = document.getElementById('workspaces-new')
const workspacesSelect = document.getElementById('workspaces-select')
const workspacesFontCssUrlInput = document.getElementById('workspaces-font-css-url')
const workspacesFontCssUrlLoad = document.getElementById('workspaces-font-css-url-load')
const workspacesOpen = document.getElementById('workspaces-open')
const workspacesRename = document.getElementById('workspaces-rename')
const workspacesRemove = document.getElementById('workspaces-remove')
Expand Down Expand Up @@ -349,6 +358,14 @@ const previewBackground = createPreviewBackgroundController({
},
})

const previewFont = createPreviewFontSetup({
createPreviewFontController,
previewFontCssUrlInput: workspacesFontCssUrlInput,
defaultPreviewFontCssUrl,
getRenderRuntime: () => renderRuntime,
queueWorkspaceSave: () => queueWorkspaceSave?.(),
})

const layoutTheme = createLayoutThemeController({
appThemeButtons,
syncPreviewBackgroundPickerFromTheme: () =>
Expand Down Expand Up @@ -757,7 +774,9 @@ const workspaceSyncController = createWorkspaceSyncController({
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
getActiveWorkspaceCreatedAt: () => activeWorkspaceCreatedAt,
getRenderModeValue: () => renderMode.value,
getPreviewFontCssUrlValue: () => previewFont.getPreviewFontCssUrl(),
normalizeRenderMode: mode => normalizeRenderMode(mode),
normalizePreviewFontCssUrl,
})

const getTypecheckSourcePath = () =>
Expand Down Expand Up @@ -891,10 +910,15 @@ const {
workspaceTabsState,
resolveWorkspaceActiveTabId,
normalizeRenderMode: mode => normalizeRenderMode(mode),
normalizePreviewFontCssUrl,
getRenderModeValue: () => renderMode.value,
getPreviewFontCssUrlValue: () => previewFont.getPreviewFontCssUrl(),
setRenderModeValue: value => {
renderMode.value = value
},
setPreviewFontCssUrlValue: (value, options) => {
previewFont.applyPreviewFontCssUrl(value, options)
},
getActiveWorkspaceTab,
onActiveWorkspaceTabChange: (_tab, { changed } = {}) => {
syncDiagnosticsDrawerLayout()
Expand Down Expand Up @@ -948,6 +972,13 @@ const {
onWorkspaceRecordApplied: onWorkspaceRecordAppliedWithStatusMetadata,
})

const applyWorkspaceFontCssUrl = createApplyWorkspaceFontCssUrl({
previewFont,
flushWorkspaceSave,
normalizePreviewFontCssUrl,
defaultPreviewFontCssUrl,
})

const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
createWorkspaceScopeForkActions({
toNonEmptyWorkspaceText,
Expand Down Expand Up @@ -1201,6 +1232,8 @@ const githubWorkflows = createGitHubWorkflowsSetup({
workspacesShare,
workspacesNew,
workspacesSelect,
workspacesFontCssUrlInput,
workspacesFontCssUrlLoad,
workspacesOpen,
workspacesRename,
workspacesRemove,
Expand All @@ -1220,6 +1253,7 @@ const githubWorkflows = createGitHubWorkflowsSetup({
listLocalContextRecords,
refreshLocalContextOptions,
applyWorkspaceRecord,
applyWorkspaceFontCssUrl,
syncActiveWorkspaceRepositoryScope,
forkWorkspaceFromCurrentState,
flushWorkspaceSave,
Expand Down Expand Up @@ -1424,6 +1458,7 @@ const runtimeCoreOptions = createRuntimeCoreOptions({
getRenderRuntime: () => renderRuntime,
getPreviewHost: () => previewHost,
previewBackground,
previewFont,
clearDiagnosticsScope,
clearConfirmDialog,
clearConfirmTitle,
Expand Down Expand Up @@ -1602,6 +1637,7 @@ bindAppEventsAndStart({
typeDiagnostics,
clipboardSupported,
previewBackground,
previewFont,
initializeCodeEditors,
},
})
20 changes: 20 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,26 @@ <h2 id="workspaces-title">Workspaces</h2>
</div>
</div>

<div class="workspaces-drawer__font-row">
<label
class="github-pr-field github-pr-field--full"
for="workspaces-font-css-url"
>
<span>Font Stylesheet URL</span>
<input
id="workspaces-font-css-url"
type="text"
autocomplete="off"
spellcheck="false"
aria-label="Workspace font stylesheet URL"
placeholder="https://cdn.example.com/fonts.css"
/>
</label>
<button class="render-button" id="workspaces-font-css-url-load" type="button">
Load
</button>
</div>

<label class="github-pr-field github-pr-field--full" for="workspaces-select">
<span>Workspace</span>
<select id="workspaces-select" aria-label="Stored workspace">
Expand Down
2 changes: 2 additions & 0 deletions src/modules/app-core/app-bindings-startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ const bindAppEventsAndStart = ({
typeDiagnostics,
clipboardSupported,
previewBackground,
previewFont,
initializeCodeEditors,
} = startup

Expand Down Expand Up @@ -495,6 +496,7 @@ const bindAppEventsAndStart = ({
syncDiagnosticsDrawerLayout()
renderRuntime.setStyleCompiling(false)
setCdnLoading(true)
previewFont.initializePreviewFontInput()
previewBackground.initializePreviewBackgroundPicker()
const workspaceRestoreReady = (async () => {
try {
Expand Down
Loading
Loading