Skip to content

Commit 186e4c0

Browse files
feat: workspaces without pat.
1 parent 49d1ce3 commit 186e4c0

13 files changed

Lines changed: 453 additions & 95 deletions

File tree

playwright/diagnostics.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,42 @@ test('typecheck does not report TS2307 for stylesheet side-effect imports', asyn
247247
expect(diagnosticsText).not.toContain("Cannot find module '../styles/app.css'")
248248
})
249249

250+
test('typecheck recognizes css module class maps in React mode', async ({ page }) => {
251+
await waitForInitialRender(page)
252+
253+
await ensurePanelToolsVisible(page, 'component')
254+
await addWorkspaceTab(page, { type: 'style' })
255+
await page.getByRole('button', { name: 'Rename tab module.css' }).click()
256+
const renameInput = page.getByLabel('Rename module.css')
257+
await renameInput.fill('app.module.css')
258+
await renameInput.press('Enter')
259+
260+
await setWorkspaceTabSource(page, {
261+
fileName: 'app.module.css',
262+
kind: 'styles',
263+
source: ['.btn {', ' color: #fff;', '}'].join('\n'),
264+
})
265+
266+
await setComponentEditorSource(
267+
page,
268+
[
269+
"import styles from '../styles/app.module.css'",
270+
'',
271+
'const App = () => <button className={styles.btn}>ok</button>',
272+
'',
273+
].join('\n'),
274+
)
275+
276+
await runTypecheck(page)
277+
await ensureDiagnosticsDrawerOpen(page)
278+
await expect(page.locator('#diagnostics-component')).toContainText(
279+
'No TypeScript errors found.',
280+
)
281+
282+
const diagnosticsText = await page.locator('#diagnostics-component').innerText()
283+
expect(diagnosticsText).not.toContain("Property 'btn' does not exist on type 'string'")
284+
})
285+
250286
test('component diagnostics rows navigate editor to reported line', async ({ page }) => {
251287
await waitForInitialRender(page)
252288

playwright/github-byot-ai.spec.ts

Lines changed: 256 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ChatRequestBody, ChatRequestMessage } from './helpers/app-test-hel
44
import {
55
appEntryPath,
66
connectByotWithSingleRepo,
7+
ensureWorkspacesDrawerClosed,
78
ensureAiChatDrawerOpen,
89
ensureOpenPrDrawerOpen,
910
mockRepositoryBranches,
@@ -12,6 +13,10 @@ import {
1213
setStylesEditorSource,
1314
waitForAppReady,
1415
} from './helpers/app-test-helpers.js'
16+
import {
17+
getAllWorkspaceRecords,
18+
seedLocalWorkspaceContexts,
19+
} from './github-pr-drawer/github-pr-drawer.helpers.js'
1520

1621
test('PR/BYOT controls are visible and chat stays hidden until token connect', async ({
1722
page,
@@ -37,7 +42,256 @@ test('PR/BYOT controls are visible and chat stays hidden until token connect', a
3742
await expect(prToggle).toHaveCount(1)
3843
await expect(prToggle).toBeHidden()
3944
await expect(workspacesToggle).toHaveCount(1)
40-
await expect(workspacesToggle).toBeHidden()
45+
await expect(workspacesToggle).toBeVisible()
46+
})
47+
48+
test('Workspaces repository filter is local-only and read-only without PAT', async ({
49+
page,
50+
}) => {
51+
await waitForAppReady(page)
52+
53+
const workspacesToggle = page.getByRole('button', {
54+
name: 'Workspaces',
55+
exact: true,
56+
})
57+
await expect(workspacesToggle).toBeVisible()
58+
59+
await workspacesToggle.click()
60+
61+
const repositoryFilter = page.getByRole('combobox', {
62+
name: 'Workspace repository filter',
63+
})
64+
await expect(repositoryFilter).toBeDisabled()
65+
await expect(repositoryFilter).toHaveValue('__local__')
66+
await expect(repositoryFilter.locator('option')).toHaveCount(1)
67+
await expect(repositoryFilter.locator('option')).toHaveText(['Local'])
68+
})
69+
70+
test('No-PAT startup restores Local workspace from mixed stored contexts', async ({
71+
page,
72+
}) => {
73+
const localWorkspaceId = 'local_no_pat_restore_target'
74+
const localHead = 'feat/local-no-pat-restore'
75+
const localMarker = 'Local restore marker content'
76+
const repositoryMarker = 'Repository restore marker content'
77+
78+
await waitForAppReady(page)
79+
80+
await page.evaluate(async () => {
81+
const request = indexedDB.open('knighted-develop-workspaces')
82+
const db = await new Promise<IDBDatabase>((resolve, reject) => {
83+
request.onsuccess = () => resolve(request.result)
84+
request.onerror = () => reject(request.error)
85+
request.onblocked = () => reject(new Error('Could not open IndexedDB.'))
86+
})
87+
88+
try {
89+
const tx = db.transaction('prWorkspaces', 'readwrite')
90+
const store = tx.objectStore('prWorkspaces')
91+
const clearRequest = store.clear()
92+
93+
await new Promise<void>((resolve, reject) => {
94+
clearRequest.onsuccess = () => resolve()
95+
clearRequest.onerror = () => reject(clearRequest.error)
96+
})
97+
98+
await new Promise<void>((resolve, reject) => {
99+
tx.oncomplete = () => resolve()
100+
tx.onerror = () => reject(tx.error)
101+
tx.onabort = () => reject(tx.error)
102+
})
103+
} finally {
104+
db.close()
105+
}
106+
})
107+
108+
await seedLocalWorkspaceContexts(page, [
109+
{
110+
id: localWorkspaceId,
111+
repo: '',
112+
workspaceScope: 'local',
113+
base: 'main',
114+
head: localHead,
115+
prTitle: 'Local restore target',
116+
prContextState: 'inactive',
117+
prNumber: null,
118+
tabs: [
119+
{
120+
id: 'entry',
121+
name: 'App.tsx',
122+
path: 'src/components/App.tsx',
123+
language: 'javascript-jsx',
124+
role: 'entry',
125+
isActive: true,
126+
content: `export const App = () => <main>${localMarker}</main>`,
127+
},
128+
],
129+
activeTabId: 'entry',
130+
createdAt: Date.now() - 5000,
131+
lastModified: Date.now() - 5000,
132+
},
133+
{
134+
id: 'repo_no_pat_restore_should_not_apply',
135+
repo: 'knightedcodemonkey/develop',
136+
workspaceScope: 'repository',
137+
base: 'main',
138+
head: 'feat/repo-should-not-restore-without-pat',
139+
prTitle: 'Repository active context',
140+
prContextState: 'active',
141+
prNumber: 107,
142+
tabs: [
143+
{
144+
id: 'entry',
145+
name: 'App.tsx',
146+
path: 'src/components/App.tsx',
147+
language: 'javascript-jsx',
148+
role: 'entry',
149+
isActive: true,
150+
content: `export const App = () => <main>${repositoryMarker}</main>`,
151+
},
152+
],
153+
activeTabId: 'entry',
154+
createdAt: Date.now() + 5000,
155+
lastModified: Date.now() + 5000,
156+
},
157+
])
158+
159+
await page.reload()
160+
await waitForAppReady(page)
161+
162+
await expect(page.locator('#github-pr-head-branch')).toHaveValue(localHead)
163+
await expect(
164+
page.getByRole('textbox', { name: 'Component source editor' }),
165+
).toContainText(localMarker)
166+
await expect(
167+
page.getByRole('textbox', { name: 'Component source editor' }),
168+
).not.toContainText(repositoryMarker)
169+
170+
const workspacesToggle = page.getByRole('button', {
171+
name: 'Workspaces',
172+
exact: true,
173+
})
174+
await workspacesToggle.click()
175+
176+
await expect(page.locator('#workspaces-repository')).toBeDisabled()
177+
await expect(page.locator('#workspaces-select')).toHaveValue(localWorkspaceId)
178+
await expect(page.getByRole('button', { name: 'Remove', exact: true })).toBeDisabled()
179+
})
180+
181+
test('PAT connect after Local-only session preserves Local records and enables repository workflows', async ({
182+
page,
183+
}) => {
184+
const localWorkspaceId = 'local_pat_connect_preserve'
185+
const localHead = 'feat/local-before-pat-connect'
186+
187+
await waitForAppReady(page)
188+
189+
await page.evaluate(async () => {
190+
const request = indexedDB.open('knighted-develop-workspaces')
191+
const db = await new Promise<IDBDatabase>((resolve, reject) => {
192+
request.onsuccess = () => resolve(request.result)
193+
request.onerror = () => reject(request.error)
194+
request.onblocked = () => reject(new Error('Could not open IndexedDB.'))
195+
})
196+
197+
try {
198+
const tx = db.transaction('prWorkspaces', 'readwrite')
199+
const store = tx.objectStore('prWorkspaces')
200+
const clearRequest = store.clear()
201+
202+
await new Promise<void>((resolve, reject) => {
203+
clearRequest.onsuccess = () => resolve()
204+
clearRequest.onerror = () => reject(clearRequest.error)
205+
})
206+
207+
await new Promise<void>((resolve, reject) => {
208+
tx.oncomplete = () => resolve()
209+
tx.onerror = () => reject(tx.error)
210+
tx.onabort = () => reject(tx.error)
211+
})
212+
} finally {
213+
db.close()
214+
}
215+
})
216+
217+
await seedLocalWorkspaceContexts(page, [
218+
{
219+
id: localWorkspaceId,
220+
repo: '',
221+
workspaceScope: 'local',
222+
base: 'main',
223+
head: localHead,
224+
prTitle: 'Local only workspace before PAT',
225+
prContextState: 'inactive',
226+
prNumber: null,
227+
createdAt: Date.now() - 1000,
228+
lastModified: Date.now() - 1000,
229+
},
230+
])
231+
232+
await page.reload()
233+
await waitForAppReady(page)
234+
235+
const workspacesToggle = page.getByRole('button', {
236+
name: 'Workspaces',
237+
exact: true,
238+
})
239+
await workspacesToggle.click()
240+
241+
const repositoryFilter = page.getByLabel('Workspace repository filter')
242+
await expect(repositoryFilter).toBeDisabled()
243+
await expect(repositoryFilter).toHaveValue('__local__')
244+
await expect(page.locator('#workspaces-select')).toHaveValue(localWorkspaceId)
245+
246+
await ensureWorkspacesDrawerClosed(page)
247+
248+
await page.route('https://api.github.com/user/repos**', async route => {
249+
await route.fulfill({
250+
status: 200,
251+
contentType: 'application/json',
252+
body: JSON.stringify([
253+
{
254+
id: 11,
255+
owner: { login: 'knightedcodemonkey' },
256+
name: 'develop',
257+
full_name: 'knightedcodemonkey/develop',
258+
default_branch: 'main',
259+
permissions: { push: true },
260+
},
261+
]),
262+
})
263+
})
264+
265+
await mockRepositoryBranches(page, {
266+
'knightedcodemonkey/develop': ['main', 'release'],
267+
})
268+
269+
await page
270+
.getByRole('textbox', { name: 'GitHub token' })
271+
.fill('github_pat_fake_transition_1234567890')
272+
await page.getByRole('button', { name: 'Add GitHub token' }).click()
273+
274+
await expect(page.getByRole('button', { name: 'Open pull request' })).toBeVisible()
275+
276+
await workspacesToggle.click()
277+
await expect(repositoryFilter).toBeEnabled()
278+
await expect(repositoryFilter.locator('option')).toHaveCount(2)
279+
await expect(repositoryFilter.locator('option')).toHaveText([
280+
'Local',
281+
'knightedcodemonkey/develop',
282+
])
283+
284+
await repositoryFilter.selectOption('knightedcodemonkey/develop')
285+
await expect(repositoryFilter).toHaveValue('knightedcodemonkey/develop')
286+
287+
const records = await getAllWorkspaceRecords(page)
288+
const localRecord = records.find(record => record?.id === localWorkspaceId)
289+
290+
expect(localRecord).toBeTruthy()
291+
expect(typeof localRecord?.repo === 'string' ? localRecord.repo : '').toBe('')
292+
expect(
293+
typeof localRecord?.workspaceScope === 'string' ? localRecord.workspaceScope : '',
294+
).toBe('local')
41295
})
42296

43297
test('chat becomes available after token connect', async ({ page }) => {
@@ -80,7 +334,7 @@ test('BYOT controls render with default app entry', async ({ page }) => {
80334
await expect(prToggle).toHaveCount(1)
81335
await expect(prToggle).toBeHidden()
82336
await expect(workspacesToggle).toHaveCount(1)
83-
await expect(workspacesToggle).toBeHidden()
337+
await expect(workspacesToggle).toBeVisible()
84338
})
85339

86340
test('GitHub token info panel reflects missing and present token states', async ({

playwright/rendering-modes/core.spec.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ test('css module imports expose class map for module tabs', async ({ page }) =>
217217
'.item {',
218218
' color: rgb(10, 20, 30);',
219219
'}',
220+
'',
221+
'.item:hover {',
222+
' color: rgb(30, 40, 50);',
223+
'}',
224+
'',
225+
'.item:active {',
226+
' color: rgb(60, 70, 80);',
227+
'}',
220228
].join('\n'),
221229
})
222230

@@ -283,6 +291,19 @@ test('css module imports expose class map for module tabs', async ({ page }) =>
283291
'color',
284292
'rgb(10, 20, 30)',
285293
)
294+
295+
await expect
296+
.poll(async () => readPreviewUserStyleText(page))
297+
.toEqual(expect.stringMatching(/\.[A-Za-z0-9_-]+_item:hover\s*\{/))
298+
await expect
299+
.poll(async () => readPreviewUserStyleText(page))
300+
.toEqual(expect.stringMatching(/\.[A-Za-z0-9_-]+_item:active\s*\{/))
301+
await expect
302+
.poll(async () => readPreviewUserStyleText(page))
303+
.not.toContain('.item:hover')
304+
await expect
305+
.poll(async () => readPreviewUserStyleText(page))
306+
.not.toContain('.item:active')
286307
})
287308

288309
test('preview styles require explicit import from entry graph', async ({ page }) => {
@@ -562,16 +583,15 @@ test('config patch keeps preview style order stable around app head styles', asy
562583
}
563584
})
564585

565-
if (!resolvedOrderBeforePatch) {
566-
throw new Error('Expected app-injected head style to exist before config patch.')
586+
expect(resolvedOrderBeforePatch).not.toBeNull()
587+
const orderBeforePatch = resolvedOrderBeforePatch as {
588+
baseIndex: number
589+
userIndex: number
590+
appIndex: number
567591
}
568592

569-
expect(resolvedOrderBeforePatch.baseIndex).toBeLessThan(
570-
resolvedOrderBeforePatch.userIndex,
571-
)
572-
expect(resolvedOrderBeforePatch.userIndex).toBeLessThan(
573-
resolvedOrderBeforePatch.appIndex,
574-
)
593+
expect(orderBeforePatch.baseIndex).toBeLessThan(orderBeforePatch.userIndex)
594+
expect(orderBeforePatch.userIndex).toBeLessThan(orderBeforePatch.appIndex)
575595

576596
await page.getByLabel('Background').fill('#456789')
577597

@@ -600,7 +620,7 @@ test('config patch keeps preview style order stable around app head styles', asy
600620
}
601621
})
602622
})
603-
.toEqual(resolvedOrderBeforePatch)
623+
.toEqual(orderBeforePatch)
604624
})
605625

606626
test('nested module imports can bring styles into preview graph', async ({ page }) => {

0 commit comments

Comments
 (0)