This commit is contained in:
Diego Imbert
2026-05-11 12:26:21 +02:00
parent fb221cb861
commit a98a339ee6
3 changed files with 243 additions and 18 deletions
+18 -18
View File
@@ -19,18 +19,18 @@
"open": "^10.0.0",
"svelte": "^5.45.2",
"tar-stream": "^3.1.7",
"windmill-parser-wasm-csharp": "*",
"windmill-parser-wasm-go": "*",
"windmill-parser-wasm-java": "*",
"windmill-parser-wasm-nu": "*",
"windmill-parser-wasm-php": "*",
"windmill-parser-wasm-py": "^1.693.1",
"windmill-parser-wasm-py-imports": "^1.693.1",
"windmill-parser-wasm-regex": "*",
"windmill-parser-wasm-ruby": "*",
"windmill-parser-wasm-rust": "*",
"windmill-parser-wasm-ts": "^1.693.1",
"windmill-parser-wasm-yaml": "*",
"windmill-parser-wasm-csharp": "1.510.1",
"windmill-parser-wasm-go": "1.510.1",
"windmill-parser-wasm-java": "1.510.1",
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.647.1",
"windmill-parser-wasm-py": "1.693.1",
"windmill-parser-wasm-py-imports": "1.693.1",
"windmill-parser-wasm-regex": "1.692.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.695.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-yaml-validator": "1.1.1",
"ws": "8.18.0",
"yaml": "^2.7.0"
@@ -1438,9 +1438,9 @@
"integrity": "sha512-FC0KbREe2G/sa/9kYIR930wmWw+VL6PvEIqg12J3dsJes3A+0x5JIUPT/jeD+c24DrG0ko/Ub7yDnYs56Bem7g=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.639.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.639.0.tgz",
"integrity": "sha512-qvYM4sYxB6M0xrqwBljS2fWqOMk6rp++60TRltJnzZDzVaWQrKjTGwNMmfepGAIWy1OGVKp0SCVERhe2P+O6tQ=="
"version": "1.692.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.692.0.tgz",
"integrity": "sha512-BHGTxrinZJ9ef6hFxbKiBqBEr5uqgG/QySOgMA5r1LswO9n/8fyGswr8JcPT2kGaoeoweV6/RQ+RHVaOhosnKw=="
},
"node_modules/windmill-parser-wasm-ruby": {
"version": "1.526.1",
@@ -1453,9 +1453,9 @@
"integrity": "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ=="
},
"node_modules/windmill-parser-wasm-ts": {
"version": "1.693.1",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.693.1.tgz",
"integrity": "sha512-xrPgVWwQbOWJKiz68wBDNMrKVOtCY/utyhzSx0kFYIWm/QH/6L8k6LLjo4DOn+PnlBNRXc7qum0sDyB8IuuJYQ=="
"version": "1.695.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.695.0.tgz",
"integrity": "sha512-9EFxeRZWmfb7EyhSlcG7dzTTKETPRYAvpRlxxLkhhtI5I219wFgI7kwrMpz4stXHJj/aqBknVv66NQHRstSJmw=="
},
"node_modules/windmill-parser-wasm-yaml": {
"version": "1.593.0",
+10
View File
@@ -61,6 +61,16 @@ Object.defineProperty(globalThis, 'sessionStorage', {
writable: true
})
// Some modules (e.g. svelte5Utils.useLocalStorageValue) gate browser-only
// behavior on `typeof window`. Provide a minimal window so they don't
// short-circuit during tests.
if (typeof (globalThis as any).window === 'undefined') {
Object.defineProperty(globalThis, 'window', {
value: globalThis,
writable: true
})
}
vi.mock('@codingame/monaco-vscode-standalone-typescript-language-features/worker', () => ({
TypeScriptWorker: class TypeScriptWorker {
private _mockScriptSnapshot?: {
+215
View File
@@ -0,0 +1,215 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
// Capture onDestroy callbacks so we can simulate component teardown without
// a real component context.
const onDestroyCallbacks: Array<() => void> = []
vi.mock('svelte', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>
return {
...actual,
onDestroy: (fn: () => void) => {
onDestroyCallbacks.push(fn)
}
}
})
// Imported AFTER vi.mock so the module sees the mocked onDestroy.
const { UserDraft, __resetUserDraftForTesting } = await import('./userDraft.svelte')
const { workspaceStore } = await import('./stores')
function flushDestroyCallbacks(): void {
const callbacks = onDestroyCallbacks.splice(0, onDestroyCallbacks.length)
for (const cb of callbacks) cb()
}
beforeEach(() => {
__resetUserDraftForTesting()
onDestroyCallbacks.length = 0
localStorage.clear()
workspaceStore.set('test_ws')
})
describe('UserDraft.save / get / remove (no observers)', () => {
it('save writes to localStorage under the workspace-scoped key', () => {
UserDraft.save('flow', 'u/me/myflow', { value: { hello: 'world' } })
const raw = localStorage.getItem('userdraft/w/test_ws/flow/u/me/myflow')
expect(raw).toBe(JSON.stringify({ value: { hello: 'world' } }))
})
it('get reads from localStorage when no observer is registered', () => {
localStorage.setItem(
'userdraft/w/test_ws/script/u/me/script1',
JSON.stringify({ value: 'code' })
)
expect(UserDraft.get('script', 'u/me/script1')).toEqual({ value: 'code' })
})
it('get returns undefined when nothing is stored', () => {
expect(UserDraft.get('flow', 'u/me/missing')).toBeUndefined()
})
it('get returns undefined when stored value is malformed', () => {
localStorage.setItem('userdraft/w/test_ws/flow/u/me/bad', 'not-json')
expect(UserDraft.get('flow', 'u/me/bad')).toBeUndefined()
})
it('remove clears the localStorage entry', () => {
UserDraft.save('app', 'u/me/app1', { value: { grid: [] } })
expect(localStorage.getItem('userdraft/w/test_ws/app/u/me/app1')).not.toBeNull()
UserDraft.remove('app', 'u/me/app1')
expect(localStorage.getItem('userdraft/w/test_ws/app/u/me/app1')).toBeNull()
})
it('uses the workspace from opts when provided', () => {
UserDraft.save('flow', 'u/me/f', { value: 1 }, { workspace: 'other_ws' })
expect(localStorage.getItem('userdraft/w/other_ws/flow/u/me/f')).toBe(
JSON.stringify({ value: 1 })
)
// Default workspace key must remain empty.
expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/f')).toBeNull()
})
it('supports trigger kinds as item kinds', () => {
UserDraft.save('kafka', 'u/me/topic1', { value: { brokers: ['localhost:9092'] } })
const raw = localStorage.getItem('userdraft/w/test_ws/kafka/u/me/topic1')
expect(raw).toBe(JSON.stringify({ value: { brokers: ['localhost:9092'] } }))
})
it('throws when neither opts.workspace nor $workspaceStore is set', () => {
workspaceStore.set(undefined)
expect(() => UserDraft.save('flow', 'u/me/x', { value: 1 })).toThrow(/no workspace/)
})
})
describe('UserDraft.use() — observer sync', () => {
it('loads the existing localStorage value on first use', () => {
localStorage.setItem(
'userdraft/w/test_ws/flow/u/me/loaded',
JSON.stringify({ value: 'preloaded' })
)
const handle = UserDraft.use<{ value: string }>('flow', 'u/me/loaded')
expect(handle.draft).toEqual({ value: 'preloaded' })
})
it('two handles on the same key share the same underlying state', () => {
const a = UserDraft.use<{ value: number }>('flow', 'u/me/shared')
const b = UserDraft.use<{ value: number }>('flow', 'u/me/shared')
a.draft = { value: 42 }
expect(b.draft).toEqual({ value: 42 })
b.draft = { value: 99 }
expect(a.draft).toEqual({ value: 99 })
})
it('save() propagates to live use() handles', () => {
const handle = UserDraft.use<{ value: number }>('flow', 'u/me/observed')
expect(handle.draft).toBeUndefined()
UserDraft.save('flow', 'u/me/observed', { value: 7 })
expect(handle.draft).toEqual({ value: 7 })
// And the underlying localStorage entry is updated too.
expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/observed')).toBe(
JSON.stringify({ value: 7 })
)
})
it('remove() propagates to live use() handles and clears localStorage', () => {
UserDraft.save('flow', 'u/me/removed', { value: 1 })
const handle = UserDraft.use<{ value: number }>('flow', 'u/me/removed')
expect(handle.draft).toEqual({ value: 1 })
UserDraft.remove('flow', 'u/me/removed')
expect(handle.draft).toBeUndefined()
expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/removed')).toBeNull()
})
it('writes through the handle setter persist to localStorage', () => {
const handle = UserDraft.use<{ value: string }>('flow', 'u/me/setter')
handle.draft = { value: 'persisted' }
expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/setter')).toBe(
JSON.stringify({ value: 'persisted' })
)
})
it('setting handle.draft = undefined removes the localStorage entry', () => {
const handle = UserDraft.use<{ value: string }>('flow', 'u/me/clear')
handle.draft = { value: 'temp' }
expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/clear')).not.toBeNull()
handle.draft = undefined
expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/clear')).toBeNull()
expect(handle.draft).toBeUndefined()
})
it('two handles in different workspaces are isolated', () => {
const a = UserDraft.use<{ value: number }>('flow', 'u/me/iso', { workspace: 'ws_a' })
const b = UserDraft.use<{ value: number }>('flow', 'u/me/iso', { workspace: 'ws_b' })
a.draft = { value: 1 }
b.draft = { value: 2 }
expect(a.draft).toEqual({ value: 1 })
expect(b.draft).toEqual({ value: 2 })
})
it('save() falls back to localStorage when no handle is registered', () => {
UserDraft.save('flow', 'u/me/noobs', { value: 'fallback' })
// First use() afterwards loads the persisted value.
const handle = UserDraft.use<{ value: string }>('flow', 'u/me/noobs')
expect(handle.draft).toEqual({ value: 'fallback' })
})
})
describe('UserDraft.use() — reference counting & cleanup', () => {
it('destroys the entry when the last handle is released', () => {
// First handle acquires the entry.
const a = UserDraft.use<{ value: number }>('flow', 'u/me/ref')
a.draft = { value: 1 }
// Second handle increments the count.
const b = UserDraft.use<{ value: number }>('flow', 'u/me/ref')
expect(b.draft).toEqual({ value: 1 })
// onDestroy for both handles got registered.
expect(onDestroyCallbacks.length).toBe(2)
// Releasing one handle keeps the entry alive — save() still updates handle a.
const firstCb = onDestroyCallbacks.shift()!
firstCb()
UserDraft.save('flow', 'u/me/ref', { value: 2 })
expect(a.draft).toEqual({ value: 2 })
// Releasing the second handle drops the entry; subsequent save()
// must go straight to localStorage rather than mutating in-memory
// state (which no longer exists).
const secondCb = onDestroyCallbacks.shift()!
secondCb()
UserDraft.save('flow', 'u/me/ref', { value: 3 })
expect(localStorage.getItem('userdraft/w/test_ws/flow/u/me/ref')).toBe(
JSON.stringify({ value: 3 })
)
})
it('a fresh use() after cleanup re-reads the latest persisted value', () => {
const a = UserDraft.use<{ value: string }>('flow', 'u/me/cycle')
a.draft = { value: 'first' }
flushDestroyCallbacks()
// After all handles release, a brand-new use() must pick up the
// value persisted to localStorage from the previous round.
const b = UserDraft.use<{ value: string }>('flow', 'u/me/cycle')
expect(b.draft).toEqual({ value: 'first' })
})
})