fix: harden OAuth identity and cookies and correct quota and JSON display

This commit is contained in:
elky
2026-09-08 10:51:25 +08:00
parent 7113d04f8a
commit cf8ea19856
24 changed files with 1327 additions and 154 deletions
@@ -169,11 +169,6 @@ function getDisplayHtml(line: JsonDisplayLine): string {
function toggleFold(line: JsonDisplayLine, index: number) {
const overrides = new Map(foldOverrides.value)
if (line.collapsed) {
for (const path of overrides.keys()) {
if (path.startsWith(`${line.id}/`)) overrides.delete(path)
}
}
overrides.set(line.id, !line.collapsed)
foldOverrides.value = overrides
localReader = undefined
@@ -10,23 +10,46 @@ const apps: Array<{ app: App, root: HTMLElement }> = []
afterEach(() => { for (const { app, root } of apps.splice(0)) { app.unmount(); root.remove() } })
function mountBody(value: unknown) {
const engine = new BodyDocumentEngine(value)
const engine = new BodyDocumentEngine(value)
const json = vi.fn(async (options: BodyJsonOptions) => engine.json(options))
const bodyDocument = shallowRef({ json } as unknown as BodyDocument)
const expandDepth = shallowRef(999)
const errors: unknown[] = []
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(defineComponent({ setup: () => () => h(JsonContent, {
data: null, bodyDocument: bodyDocument.value, expandDepth: 999, viewMode: 'formatted', isDark: false, emptyMessage: '无数据',
data: null, bodyDocument: bodyDocument.value, expandDepth: expandDepth.value, viewMode: 'formatted', isDark: false, emptyMessage: '无数据',
onLoadError: (error: unknown) => errors.push(error),
}) }))
app.mount(root)
apps.push({ app, root })
return { root, json, bodyDocument, engine, errors }
return { root, json, bodyDocument, expandDepth, engine, errors }
}
function button(root: HTMLElement, label: string) { return [...root.querySelectorAll('button')].find(button => button.textContent?.includes(label))! }
describe('worker-backed body views', () => {
it('resets every layer on collapse-all and reopens worker-backed nodes one layer at a time', async () => {
const value = { messages: [{ content: { text: 'deep worker content' } }] }
const { root, json, expandDepth, engine } = mountBody(value)
await vi.waitFor(() => expect(root.textContent).toContain('deep worker content'))
for (let cycle = 0; cycle < 2; cycle += 1) {
expandDepth.value = 0
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(3))
expect(json).toHaveBeenLastCalledWith(expect.objectContaining({ expandDepth: 0, foldOverrides: new Map() }))
for (const lineCount of [5, 7, 9]) {
expect(root.textContent).not.toContain('deep worker content')
root.querySelector<HTMLButtonElement>('button[aria-label="展开节点"]')!.click()
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(lineCount))
}
expect(root.textContent).toContain('deep worker content')
expandDepth.value = 999
await vi.waitFor(() => expect(json).toHaveBeenLastCalledWith(expect.objectContaining({ expandDepth: 999, foldOverrides: new Map() })))
await vi.waitFor(() => expect(root.textContent).toContain('deep worker content'))
expect(root.querySelector('button[aria-label="展开节点"]')).toBeNull()
}
expect(JSON.parse(engine.copy())).toEqual(value)
})
it('automatically reads worker chunks while scrolling and preserves the complete copy', async () => {
const values = Array.from({ length: 1000 }, (_value, index) => `value-${index}`)
const { root, json, engine } = mountBody(values)
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, shallowRef, type App } from 'vue'
import JsonContent from '../RequestDetailDrawer/JsonContent.vue'
import JsonContentPanel from '../JsonContentPanel.vue'
import { JSON_PAGE_SIZE, JSON_SCROLL_CHUNK_SIZE, JSON_TEXT_CHUNK_SIZE } from '../../utils/json-viewer'
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
@@ -48,15 +49,57 @@ describe('JsonContent lazy rendering', () => {
expect(root.querySelector<HTMLElement>('.virtual-body-scroll')?.scrollTop).toBe(0)
})
it('opens the complete subtree with one bracket click', async () => {
const { root } = mountJson({ messages: [{ content: 'hidden content' }] }, 0)
it('opens one layer per bracket click after collapsing all descendants', async () => {
const { root } = mountJson({ messages: [{ content: { text: 'hidden content' } }] }, 0)
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(3))
expect(root.textContent).not.toContain('hidden content')
root.querySelector<HTMLButtonElement>('button[aria-label="展开节点"]')!.click()
for (const lineCount of [5, 7, 9]) {
expect(root.textContent).not.toContain('hidden content')
root.querySelector<HTMLElement>('.line-content.clickable-collapsed')!.click()
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(lineCount))
}
await vi.waitFor(() => expect(root.textContent).toContain('hidden content'))
expect(root.querySelector('button[aria-label="展开节点"]')).toBeNull()
})
it('collapses every layer through the toolbar and keeps expand-all working', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(defineComponent({ setup: () => () => h(JsonContentPanel, {
data: { messages: [{ content: { text: 'deep content' } }] }, isDark: false,
}) }))
app.mount(root)
mountedApps.push({ app, root })
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(3))
for (let cycle = 0; cycle < 2; cycle += 1) {
root.querySelector<HTMLButtonElement>('button[title="展开全部"]')!.click()
await vi.waitFor(() => expect(root.textContent).toContain('deep content'))
expect(root.querySelector('button[aria-label="展开节点"]')).toBeNull()
root.querySelector<HTMLButtonElement>('button[title="收缩全部"]')!.click()
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(3))
for (const lineCount of [5, 7]) {
root.querySelector<HTMLButtonElement>('button[aria-label="展开节点"]')!.click()
await vi.waitFor(() => expect(root.querySelectorAll('.json-line')).toHaveLength(lineCount))
expect(root.textContent).not.toContain('deep content')
}
}
})
it('preserves an explicitly folded child when its parent is reopened', async () => {
const { root } = mountJson({ messages: [{ content: { text: 'hidden content' } }] })
const nodeButton = (key: string) => [...root.querySelectorAll('.json-line')]
.find(line => line.querySelector('.token-key')?.textContent === JSON.stringify(key))!
.querySelector<HTMLButtonElement>('button')!
await vi.waitFor(() => expect(root.textContent).toContain('hidden content'))
nodeButton('content').click()
await vi.waitFor(() => expect(nodeButton('content').getAttribute('aria-expanded')).toBe('false'))
nodeButton('messages').click()
await vi.waitFor(() => expect(nodeButton('messages').getAttribute('aria-expanded')).toBe('false'))
nodeButton('messages').click()
await vi.waitFor(() => expect(nodeButton('messages').getAttribute('aria-expanded')).toBe('true'))
expect(nodeButton('content').getAttribute('aria-expanded')).toBe('false')
expect(root.textContent).not.toContain('hidden content')
})
it('shows complete long strings without an extra expansion button', async () => {
const content = `${'x'.repeat(JSON_TEXT_CHUNK_SIZE * 10) }末尾🙂`
const { root, data } = mountJson({ content })
@@ -65,11 +65,34 @@ describe('lazy JSON pages', () => {
expect(reader.read(21).lines).toEqual([])
})
it('expands all descendants of an opened node while honoring an explicitly closed child', () => {
it.each([false, true])('opens collapsed descendants one layer at a time with indexPaths=%s', indexPaths => {
const data = { messages: [{ content: { text: 'complete content' } }] }
const opened = getJsonPage(data, { expandDepth: 1, foldOverrides: new Map([['$/messages', false]]) })
const paths = indexPaths ? ['$/0', '$/0/0', '$/0/0/0'] : ['$/messages', '$/messages/0', '$/messages/0/content']
const foldOverrides = new Map<string, boolean>()
for (const path of paths) {
const folded = getJsonPage(data, { expandDepth: 0, foldOverrides, indexPaths })
expect(folded.lines.find(line => line.id === path)).toMatchObject({ collapsed: true })
expect(folded.lines.some(line => line.value === 'complete content')).toBe(false)
foldOverrides.set(path, false)
}
const opened = getJsonPage(data, { expandDepth: 0, foldOverrides, indexPaths })
expect(opened.lines.some(line => line.value === 'complete content')).toBe(true)
const folded = getJsonPage(data, { expandDepth: 1, foldOverrides: new Map([['$/messages', false], ['$/messages/0/content', true]]) })
})
it('does not read unopened descendants after opening their parent', () => {
const readContent = vi.fn(() => { throw new Error('unopened content must not be read') })
const message = Object.defineProperty({}, 'content', { enumerable: true, get: readContent })
const opened = getJsonPage({ messages: [message] }, { expandDepth: 0, foldOverrides: new Map([['$/messages', false]]) })
expect(opened.lines.find(line => line.id === '$/messages/0')).toMatchObject({ collapsed: true })
expect(readContent).not.toHaveBeenCalled()
})
it('still expands every layer in expand-all mode while honoring explicitly closed children', () => {
const data = { messages: [{ content: { text: 'complete content' } }] }
const expanded = getJsonPage(data, { expandDepth: 999 })
expect(expanded.lines.some(line => line.value === 'complete content')).toBe(true)
expect(expanded.lines.filter(line => line.canFold).every(line => !line.collapsed)).toBe(true)
const folded = getJsonPage(data, { expandDepth: 999, foldOverrides: new Map([['$/messages', false], ['$/messages/0/content', true]]) })
expect(folded.lines.some(line => line.value === 'complete content')).toBe(false)
})
@@ -130,7 +130,7 @@ function* splitJsonLine(line: JsonTreeLine, chunkSize: number): Generator<JsonTr
function* walkJsonLines(data: unknown, options: JsonPageOptions): Generator<JsonDisplayLine> {
const depth = options.expandDepth === 0 || options.expandDepth == null ? 1 : options.expandDepth
function* walk(value: unknown, path: string, indent: number, comma: string, key?: string, expandedParent = false): Generator<JsonTreeLine> {
function* walk(value: unknown, path: string, indent: number, comma: string, key?: string): Generator<JsonTreeLine> {
if (value == null || typeof value !== 'object') {
yield { id: path, indent, key, value, comma, canFold: false, collapsed: false }
return
@@ -140,8 +140,7 @@ function* walkJsonLines(data: unknown, options: JsonPageOptions): Generator<Json
const keys = isArray ? [] : Object.keys(value)
const childCount = isArray ? value.length : keys.length
const override = options.foldOverrides?.get(path)
const expandedSubtree = expandedParent || override === false
const collapsed = childCount === 0 || (override ?? (!expandedSubtree && indent >= depth))
const collapsed = childCount === 0 || (override ?? (indent >= depth))
const bracket = isArray ? '[' : '{'
const closingBracket = isArray ? ']' : '}'
yield { id: path, indent, key, comma, bracket, closingBracket, childCount, isArray, collapsed, canFold: childCount > 0 }
@@ -151,7 +150,7 @@ function* walkJsonLines(data: unknown, options: JsonPageOptions): Generator<Json
const childKey = isArray ? String(index) : keys[index]
const childPath = `${path}/${options.indexPaths ? index : childKey.replace(/~/g, '~0').replace(/\//g, '~1')}`
const childValue = isArray ? value[index] : (value as Record<string, unknown>)[childKey]
yield* walk(childValue, childPath, indent + 1, index === childCount - 1 ? '' : ',', isArray ? undefined : childKey, expandedSubtree)
yield* walk(childValue, childPath, indent + 1, index === childCount - 1 ? '' : ',', isArray ? undefined : childKey)
}
yield { id: `close:${path}`, indent, bracket: closingBracket, comma, canFold: false, collapsed: false }
}