Skip to content

Commit e79dda9

Browse files
feat: share urls. (#137)
1 parent 906641d commit e79dda9

12 files changed

Lines changed: 780 additions & 2 deletions

File tree

.github/instructions/pr-review.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ You are reviewing changes for @knighted/develop. Be concise, technical, and spec
2323

2424
## What to verify
2525

26+
- No changes reintroduce cross-workspace overwrite/delete behavior.
2627
- No generated artifacts are edited (dist/, coverage/, test-results/).
2728
- Duplicated logic paths are avoided when a shared helper/module already exists; prefer reusing the established implementation.
2829
- CDN import/fallback behavior is not bypassed with ad hoc URLs in feature modules.

playwright/share-workspace.spec.ts

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import { expect, test } from '@playwright/test'
2+
import {
3+
appEntryPath,
4+
connectByotWithSingleRepo,
5+
resetWorkbenchStorage,
6+
setComponentEditorSource,
7+
waitForAppReady,
8+
} from './helpers/app-test-helpers.js'
9+
const installClipboardCapture = async (page: import('@playwright/test').Page) => {
10+
await page.addInitScript(() => {
11+
let copied = ''
12+
13+
Object.defineProperty(window, '__shareClipboardText', {
14+
configurable: true,
15+
get: () => copied,
16+
set: value => {
17+
copied = typeof value === 'string' ? value : String(value ?? '')
18+
},
19+
})
20+
21+
Object.defineProperty(navigator, 'clipboard', {
22+
configurable: true,
23+
value: {
24+
writeText: async (text: string) => {
25+
;(window as { __shareClipboardText?: string }).__shareClipboardText =
26+
typeof text === 'string' ? text : String(text ?? '')
27+
},
28+
readText: async () => {
29+
return (window as { __shareClipboardText?: string }).__shareClipboardText ?? ''
30+
},
31+
},
32+
})
33+
})
34+
}
35+
36+
const getWorkspaceRecords = async (page: import('@playwright/test').Page) => {
37+
return page.evaluate(async () => {
38+
const db = await new Promise<IDBDatabase>((resolve, reject) => {
39+
const request = indexedDB.open('knighted-develop-workspaces')
40+
request.onsuccess = () => resolve(request.result)
41+
request.onerror = () => reject(request.error)
42+
request.onblocked = () => reject(new Error('Could not open IndexedDB.'))
43+
})
44+
45+
try {
46+
const transaction = db.transaction('prWorkspaces', 'readonly')
47+
const store = transaction.objectStore('prWorkspaces')
48+
const request = store.getAll()
49+
50+
return await new Promise<Array<Record<string, unknown>>>((resolve, reject) => {
51+
request.onsuccess = () => {
52+
const value = Array.isArray(request.result) ? request.result : []
53+
resolve(value as Array<Record<string, unknown>>)
54+
}
55+
request.onerror = () => reject(request.error)
56+
})
57+
} finally {
58+
db.close()
59+
}
60+
})
61+
}
62+
63+
const encodeSharePayload = async (
64+
page: import('@playwright/test').Page,
65+
snapshot: Record<string, unknown>,
66+
) => {
67+
return page.evaluate(async sourceSnapshot => {
68+
const toBase64Url = (bytes: Uint8Array) => {
69+
const chunkSize = 0x8000
70+
let binary = ''
71+
for (let index = 0; index < bytes.length; index += chunkSize) {
72+
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize))
73+
}
74+
75+
const base64 = btoa(binary)
76+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
77+
}
78+
79+
const source = JSON.stringify({
80+
version: 1,
81+
compression: 'gzip',
82+
createdAt: Date.now(),
83+
snapshot: sourceSnapshot,
84+
})
85+
86+
const sourceBytes = new TextEncoder().encode(source)
87+
const sourceStream = new Blob([sourceBytes]).stream()
88+
const compressedStream = sourceStream.pipeThrough(new CompressionStream('gzip'))
89+
const compressedBuffer = await new Response(compressedStream).arrayBuffer()
90+
return toBase64Url(new Uint8Array(compressedBuffer))
91+
}, snapshot)
92+
}
93+
94+
test('share button is shown for local workspace and copies a share URL', async ({
95+
page,
96+
}) => {
97+
await installClipboardCapture(page)
98+
await waitForAppReady(page, `${appEntryPath}`)
99+
100+
await page.getByRole('button', { name: 'Workspaces' }).click()
101+
102+
const shareButton = page
103+
.locator('#workspaces-drawer')
104+
.getByRole('button', { name: 'Share local workspace snapshot' })
105+
await expect(shareButton).toBeVisible()
106+
107+
await setComponentEditorSource(
108+
page,
109+
"export const App = () => <main data-share='ready'>Shared local snapshot</main>",
110+
)
111+
112+
await shareButton.click()
113+
await expect(page.getByRole('status', { name: 'App status' })).toContainText(
114+
'Share link copied',
115+
)
116+
117+
await expect
118+
.poll(async () => {
119+
return page.evaluate(() => {
120+
return (window as { __shareClipboardText?: string }).__shareClipboardText ?? ''
121+
})
122+
})
123+
.not.toBe('')
124+
125+
const copiedUrl = await page.evaluate(() => {
126+
return (window as { __shareClipboardText?: string }).__shareClipboardText ?? ''
127+
})
128+
129+
const copiedPayload = new URL(copiedUrl).searchParams.get('sws')
130+
expect(copiedPayload).toBeTruthy()
131+
})
132+
133+
test('share button appears in workspaces drawer and not in editor controls', async ({
134+
page,
135+
}) => {
136+
await waitForAppReady(page, `${appEntryPath}`)
137+
138+
await expect(
139+
page
140+
.locator('#editor-header-component')
141+
.getByRole('button', { name: 'Share local workspace snapshot' }),
142+
).toHaveCount(0)
143+
144+
await page.getByRole('button', { name: 'Workspaces' }).click()
145+
146+
await expect(
147+
page
148+
.locator('#workspaces-drawer')
149+
.getByRole('button', { name: 'Share local workspace snapshot' }),
150+
).toBeVisible()
151+
})
152+
153+
test('share button is hidden in drawer for non-Local repository filter', async ({
154+
page,
155+
}) => {
156+
await waitForAppReady(page, `${appEntryPath}`)
157+
await connectByotWithSingleRepo(page)
158+
159+
await page.getByRole('button', { name: 'Workspaces' }).click()
160+
161+
const repositoryFilter = page.getByLabel('Workspace repository filter')
162+
await expect(repositoryFilter).toHaveValue('knightedcodemonkey/develop')
163+
164+
const drawerShareButton = page
165+
.locator('#workspaces-drawer')
166+
.getByRole('button', { name: 'Share local workspace snapshot' })
167+
await expect(drawerShareButton).toBeHidden()
168+
})
169+
170+
test('loads shared URL snapshot into IDB as a new local workspace and clears URL param', async ({
171+
page,
172+
}) => {
173+
await installClipboardCapture(page)
174+
await resetWorkbenchStorage(page)
175+
176+
const sharedSnapshot = {
177+
id: 'external-source-id',
178+
workspaceScope: 'repository',
179+
repo: 'knightedcodemonkey/develop',
180+
base: 'main',
181+
head: 'shared/feature-branch',
182+
prNumber: 123,
183+
prTitle: 'Imported snapshot',
184+
prContextState: 'active',
185+
renderMode: 'dom',
186+
tabs: [
187+
{
188+
id: 'entry',
189+
name: 'SharedEntry.tsx',
190+
path: 'src/components/SharedEntry.tsx',
191+
language: 'javascript-jsx',
192+
role: 'entry',
193+
isActive: true,
194+
content: 'export const App = () => <main>Shared import content</main>',
195+
},
196+
],
197+
activeTabId: 'entry',
198+
createdAt: Date.now() - 1000,
199+
lastModified: Date.now() - 1000,
200+
schemaVersion: 1,
201+
}
202+
203+
const encodedPayload = await encodeSharePayload(page, sharedSnapshot)
204+
await waitForAppReady(page, `${appEntryPath}?sws=${encodeURIComponent(encodedPayload)}`)
205+
206+
await expect
207+
.poll(() => {
208+
const currentUrl = new URL(page.url())
209+
return currentUrl.searchParams.has('sws')
210+
})
211+
.toBe(false)
212+
213+
await expect
214+
.poll(async () => {
215+
const records = await getWorkspaceRecords(page)
216+
const imported = records.find(record => {
217+
if (!record || typeof record !== 'object') {
218+
return false
219+
}
220+
221+
const tabs = Array.isArray(record.tabs) ? record.tabs : []
222+
return tabs.some(tab => {
223+
if (!tab || typeof tab !== 'object') {
224+
return false
225+
}
226+
227+
return (
228+
typeof tab.content === 'string' &&
229+
tab.content.includes('Shared import content')
230+
)
231+
})
232+
})
233+
234+
if (!imported || typeof imported !== 'object') {
235+
return null
236+
}
237+
238+
const tabs = Array.isArray(imported.tabs) ? imported.tabs : []
239+
const firstTab = tabs[0] && typeof tabs[0] === 'object' ? tabs[0] : null
240+
return {
241+
workspaceScope:
242+
typeof imported.workspaceScope === 'string' ? imported.workspaceScope : '',
243+
repo: typeof imported.repo === 'string' ? imported.repo : '',
244+
prNumber: imported.prNumber,
245+
prContextState:
246+
typeof imported.prContextState === 'string' ? imported.prContextState : '',
247+
hasImportedContent: tabs.some(tab => {
248+
if (!tab || typeof tab !== 'object') {
249+
return false
250+
}
251+
252+
return (
253+
typeof tab.content === 'string' &&
254+
tab.content.includes('Shared import content')
255+
)
256+
}),
257+
firstTabContent:
258+
firstTab && typeof firstTab.content === 'string' ? firstTab.content : '',
259+
}
260+
})
261+
.toEqual({
262+
workspaceScope: 'local',
263+
repo: '',
264+
prNumber: null,
265+
prContextState: 'inactive',
266+
hasImportedContent: true,
267+
firstTabContent: expect.any(String),
268+
})
269+
})
270+
271+
test('invalid shared payload does not crash app and keeps URL param for retry', async ({
272+
page,
273+
}) => {
274+
await installClipboardCapture(page)
275+
await resetWorkbenchStorage(page)
276+
277+
await waitForAppReady(page, `${appEntryPath}?sws=this-is-not-valid`)
278+
279+
await expect(page.getByRole('button', { name: 'Open tab App.tsx' })).toBeVisible()
280+
281+
await expect
282+
.poll(() => {
283+
const currentUrl = new URL(page.url())
284+
return currentUrl.searchParams.get('sws')
285+
})
286+
.toBe('this-is-not-valid')
287+
})

0 commit comments

Comments
 (0)