fix desktop doubao history cleanup
This commit is contained in:
@@ -13,6 +13,7 @@ const {
|
|||||||
isNonCitationAssetDomain,
|
isNonCitationAssetDomain,
|
||||||
isNonCitationAssetUrl,
|
isNonCitationAssetUrl,
|
||||||
hasDoubaoFinishedStreamCapture,
|
hasDoubaoFinishedStreamCapture,
|
||||||
|
deleteDoubaoCurrentConversationInPage,
|
||||||
parseDoubaoCaptures,
|
parseDoubaoCaptures,
|
||||||
parseDoubaoStreamBody,
|
parseDoubaoStreamBody,
|
||||||
resolveDoubaoSubmissionAnswer,
|
resolveDoubaoSubmissionAnswer,
|
||||||
@@ -140,6 +141,660 @@ describe('doubao adapter helpers', () => {
|
|||||||
expect(doubaoModeProviderModel('expert')).toBe('doubao-web-fast')
|
expect(doubaoModeProviderModel('expert')).toBe('doubao-web-fast')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('clicks through Doubao current conversation deletion controls', async () => {
|
||||||
|
const previousWindow = globalThis.window
|
||||||
|
const previousDocument = globalThis.document
|
||||||
|
const previousHTMLElement = globalThis.HTMLElement
|
||||||
|
const previousMouseEvent = globalThis.MouseEvent
|
||||||
|
const previousURL = globalThis.URL
|
||||||
|
|
||||||
|
const makeElement = (input: {
|
||||||
|
tagName?: string
|
||||||
|
text?: string
|
||||||
|
attrs?: Record<string, string>
|
||||||
|
rect?: { left: number; top: number; width: number; height: number }
|
||||||
|
visible?: boolean
|
||||||
|
className?: string
|
||||||
|
onClick?: () => void
|
||||||
|
children?: MockElement[]
|
||||||
|
}): MockElement => new MockElement(input)
|
||||||
|
|
||||||
|
class MockMouseEvent {
|
||||||
|
readonly type: string
|
||||||
|
constructor(type: string) {
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockElement {
|
||||||
|
tagName: string
|
||||||
|
innerText: string
|
||||||
|
textContent: string
|
||||||
|
className: string
|
||||||
|
parentElement: MockElement | null = null
|
||||||
|
children: MockElement[] = []
|
||||||
|
tabIndex = -1
|
||||||
|
style = {
|
||||||
|
display: 'block',
|
||||||
|
visibility: 'visible',
|
||||||
|
opacity: '1',
|
||||||
|
cursor: 'default',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly attrs: Map<string, string>
|
||||||
|
private readonly rect: { left: number; top: number; width: number; height: number }
|
||||||
|
private readonly onClick?: () => void
|
||||||
|
|
||||||
|
constructor(input: {
|
||||||
|
tagName?: string
|
||||||
|
text?: string
|
||||||
|
attrs?: Record<string, string>
|
||||||
|
rect?: { left: number; top: number; width: number; height: number }
|
||||||
|
visible?: boolean
|
||||||
|
className?: string
|
||||||
|
onClick?: () => void
|
||||||
|
children?: MockElement[]
|
||||||
|
}) {
|
||||||
|
this.tagName = (input.tagName ?? 'div').toUpperCase()
|
||||||
|
this.innerText = input.text ?? ''
|
||||||
|
this.textContent = input.text ?? ''
|
||||||
|
this.className = input.className ?? ''
|
||||||
|
this.attrs = new Map(Object.entries(input.attrs ?? {}))
|
||||||
|
this.rect = input.rect ?? { left: 0, top: 0, width: 120, height: 32 }
|
||||||
|
this.onClick = input.onClick
|
||||||
|
if (input.visible === false) {
|
||||||
|
this.style.display = 'none'
|
||||||
|
}
|
||||||
|
for (const child of input.children ?? []) {
|
||||||
|
this.appendChild(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendChild(child: MockElement): void {
|
||||||
|
child.parentElement = this
|
||||||
|
this.children.push(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttribute(name: string): string | null {
|
||||||
|
return this.attrs.get(name) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAttribute(name: string): boolean {
|
||||||
|
return this.attrs.has(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelector(selector: string): MockElement | null {
|
||||||
|
return this.querySelectorAll(selector)[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelectorAll(selector: string): MockElement[] {
|
||||||
|
const selectors = selector
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
const results: MockElement[] = []
|
||||||
|
const visit = (node: MockElement) => {
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (selectors.some((item) => matchesSelector(child, item))) {
|
||||||
|
results.push(child)
|
||||||
|
}
|
||||||
|
visit(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(this)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
getBoundingClientRect(): {
|
||||||
|
left: number
|
||||||
|
top: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
right: number
|
||||||
|
bottom: number
|
||||||
|
} {
|
||||||
|
let current: MockElement | null = this
|
||||||
|
while (current) {
|
||||||
|
if (current.style.display === 'none' || current.style.visibility === 'hidden') {
|
||||||
|
return {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...this.rect,
|
||||||
|
right: this.rect.left + this.rect.width,
|
||||||
|
bottom: this.rect.top + this.rect.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollIntoView(): void {}
|
||||||
|
|
||||||
|
dispatchEvent(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
click(): void {
|
||||||
|
this.onClick?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesSelector = (element: MockElement, selector: string): boolean => {
|
||||||
|
if (selector === '*') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (/^[a-z]+$/i.test(selector)) {
|
||||||
|
return element.tagName.toLowerCase() === selector.toLowerCase()
|
||||||
|
}
|
||||||
|
const tagMatch = selector.match(/^([a-z]+)(.+)$/i)
|
||||||
|
if (tagMatch) {
|
||||||
|
return (
|
||||||
|
element.tagName.toLowerCase() === tagMatch[1]?.toLowerCase() &&
|
||||||
|
matchesSelector(element, tagMatch[2] ?? '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const attrMatch = selector.match(/^\[([^=\]~*^$]+)(?:[*^$]?=['"]?([^'"\]]+)['"]?)?\]$/)
|
||||||
|
if (attrMatch) {
|
||||||
|
const attr = attrMatch[1] ?? ''
|
||||||
|
const expected = attrMatch[2]
|
||||||
|
const value = element.getAttribute(attr)
|
||||||
|
if (expected === undefined) {
|
||||||
|
return value !== null
|
||||||
|
}
|
||||||
|
if (selector.includes('*=')) {
|
||||||
|
return value?.includes(expected) ?? false
|
||||||
|
}
|
||||||
|
return value === expected
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentRoot = makeElement({ rect: { left: 0, top: 0, width: 1280, height: 900 } })
|
||||||
|
const body = makeElement({ rect: { left: 0, top: 0, width: 1280, height: 900 } })
|
||||||
|
documentRoot.appendChild(body)
|
||||||
|
|
||||||
|
let dialog: MockElement | null = null
|
||||||
|
let currentHistory: MockElement | null = null
|
||||||
|
let confirmed = false
|
||||||
|
|
||||||
|
const confirmButton = makeElement({
|
||||||
|
tagName: 'button',
|
||||||
|
text: '删除',
|
||||||
|
attrs: { role: 'button' },
|
||||||
|
rect: { left: 680, top: 478, width: 80, height: 32 },
|
||||||
|
onClick: () => {
|
||||||
|
confirmed = true
|
||||||
|
if (dialog) {
|
||||||
|
body.children = body.children.filter((child) => child !== dialog)
|
||||||
|
}
|
||||||
|
if (currentHistory) {
|
||||||
|
body.children = body.children.filter((child) => child !== currentHistory)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
dialog = makeElement({
|
||||||
|
tagName: 'div',
|
||||||
|
text: '确定删除对话 删除后不可恢复 取消 删除',
|
||||||
|
attrs: { role: 'dialog', 'aria-modal': 'true' },
|
||||||
|
rect: { left: 440, top: 320, width: 400, height: 220 },
|
||||||
|
visible: false,
|
||||||
|
children: [
|
||||||
|
makeElement({
|
||||||
|
tagName: 'button',
|
||||||
|
text: '取消',
|
||||||
|
attrs: { role: 'button' },
|
||||||
|
rect: { left: 580, top: 478, width: 80, height: 32 },
|
||||||
|
}),
|
||||||
|
confirmButton,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteItem = makeElement({
|
||||||
|
tagName: 'div',
|
||||||
|
text: '删除',
|
||||||
|
attrs: { role: 'menuitem' },
|
||||||
|
rect: { left: 240, top: 124, width: 96, height: 32 },
|
||||||
|
onClick: () => {
|
||||||
|
dialog!.style.display = 'block'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const menu = makeElement({
|
||||||
|
tagName: 'div',
|
||||||
|
text: '删除',
|
||||||
|
className: 'dropdown-menu',
|
||||||
|
rect: { left: 228, top: 112, width: 128, height: 48 },
|
||||||
|
visible: false,
|
||||||
|
children: [deleteItem],
|
||||||
|
})
|
||||||
|
|
||||||
|
const moreButton = makeElement({
|
||||||
|
tagName: 'button',
|
||||||
|
text: '...',
|
||||||
|
attrs: { 'aria-label': '更多', role: 'button' },
|
||||||
|
rect: { left: 252, top: 74, width: 28, height: 28 },
|
||||||
|
onClick: () => {
|
||||||
|
menu.style.display = 'block'
|
||||||
|
deleteItem.style.display = 'block'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
currentHistory = makeElement({
|
||||||
|
tagName: 'div',
|
||||||
|
text: '室内门锁供货商',
|
||||||
|
attrs: { 'data-active': 'true' },
|
||||||
|
rect: { left: 16, top: 64, width: 272, height: 48 },
|
||||||
|
className: 'history-item active',
|
||||||
|
children: [
|
||||||
|
makeElement({
|
||||||
|
tagName: 'a',
|
||||||
|
text: '室内门锁供货商',
|
||||||
|
attrs: { href: '/chat/local_4995117315817271' },
|
||||||
|
rect: { left: 24, top: 72, width: 210, height: 28 },
|
||||||
|
}),
|
||||||
|
moreButton,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
body.appendChild(currentHistory)
|
||||||
|
body.appendChild(menu)
|
||||||
|
body.appendChild(dialog)
|
||||||
|
|
||||||
|
const documentMock = {
|
||||||
|
body,
|
||||||
|
querySelectorAll: (selector: string) => body.querySelectorAll(selector),
|
||||||
|
querySelector: (selector: string) => body.querySelector(selector),
|
||||||
|
}
|
||||||
|
const windowMock = {
|
||||||
|
location: {
|
||||||
|
href: 'https://www.doubao.com/chat/local_4995117315817271',
|
||||||
|
origin: 'https://www.doubao.com',
|
||||||
|
},
|
||||||
|
innerWidth: 1280,
|
||||||
|
innerHeight: 900,
|
||||||
|
setTimeout: (callback: () => void) => {
|
||||||
|
callback()
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
getComputedStyle: (element: MockElement) => element.style,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mutableGlobal = globalThis as Record<string, unknown>
|
||||||
|
mutableGlobal.window = windowMock
|
||||||
|
mutableGlobal.document = documentMock
|
||||||
|
mutableGlobal.HTMLElement = MockElement
|
||||||
|
mutableGlobal.MouseEvent = MockMouseEvent
|
||||||
|
mutableGlobal.URL = URL
|
||||||
|
|
||||||
|
const result = await deleteDoubaoCurrentConversationInPage({
|
||||||
|
conversationId: 'local_4995117315817271',
|
||||||
|
pageUrl: 'https://www.doubao.com/chat/local_4995117315817271',
|
||||||
|
title: '豆包',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
conversation_id: 'local_4995117315817271',
|
||||||
|
menu_clicked: true,
|
||||||
|
delete_clicked: true,
|
||||||
|
confirm_clicked: true,
|
||||||
|
})
|
||||||
|
expect(confirmed).toBe(true)
|
||||||
|
} finally {
|
||||||
|
const mutableGlobal = globalThis as Record<string, unknown>
|
||||||
|
mutableGlobal.window = previousWindow
|
||||||
|
mutableGlobal.document = previousDocument
|
||||||
|
mutableGlobal.HTMLElement = previousHTMLElement
|
||||||
|
mutableGlobal.MouseEvent = previousMouseEvent
|
||||||
|
mutableGlobal.URL = previousURL
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the generated Doubao history link when cleanup only has the question text', async () => {
|
||||||
|
const previousWindow = globalThis.window
|
||||||
|
const previousDocument = globalThis.document
|
||||||
|
const previousHTMLElement = globalThis.HTMLElement
|
||||||
|
const previousMouseEvent = globalThis.MouseEvent
|
||||||
|
const previousURL = globalThis.URL
|
||||||
|
|
||||||
|
const makeElement = (input: {
|
||||||
|
tagName?: string
|
||||||
|
text?: string
|
||||||
|
attrs?: Record<string, string>
|
||||||
|
rect?: { left: number; top: number; width: number; height: number }
|
||||||
|
visible?: boolean
|
||||||
|
className?: string
|
||||||
|
onClick?: () => void
|
||||||
|
children?: MockElement[]
|
||||||
|
}): MockElement => new MockElement(input)
|
||||||
|
|
||||||
|
class MockMouseEvent {
|
||||||
|
readonly type: string
|
||||||
|
constructor(type: string) {
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockElement {
|
||||||
|
tagName: string
|
||||||
|
innerText: string
|
||||||
|
textContent: string
|
||||||
|
className: string
|
||||||
|
parentElement: MockElement | null = null
|
||||||
|
children: MockElement[] = []
|
||||||
|
tabIndex = -1
|
||||||
|
style = {
|
||||||
|
display: 'block',
|
||||||
|
visibility: 'visible',
|
||||||
|
opacity: '1',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly attrs: Map<string, string>
|
||||||
|
private readonly rect: { left: number; top: number; width: number; height: number }
|
||||||
|
private readonly onClick?: () => void
|
||||||
|
|
||||||
|
constructor(input: {
|
||||||
|
tagName?: string
|
||||||
|
text?: string
|
||||||
|
attrs?: Record<string, string>
|
||||||
|
rect?: { left: number; top: number; width: number; height: number }
|
||||||
|
visible?: boolean
|
||||||
|
className?: string
|
||||||
|
onClick?: () => void
|
||||||
|
children?: MockElement[]
|
||||||
|
}) {
|
||||||
|
this.tagName = (input.tagName ?? 'div').toUpperCase()
|
||||||
|
this.innerText = input.text ?? ''
|
||||||
|
this.textContent = input.text ?? ''
|
||||||
|
this.className = input.className ?? ''
|
||||||
|
this.attrs = new Map(Object.entries(input.attrs ?? {}))
|
||||||
|
this.rect = input.rect ?? { left: 0, top: 0, width: 120, height: 32 }
|
||||||
|
this.onClick = input.onClick
|
||||||
|
if (input.visible === false) {
|
||||||
|
this.style.display = 'none'
|
||||||
|
}
|
||||||
|
for (const child of input.children ?? []) {
|
||||||
|
this.appendChild(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendChild(child: MockElement): void {
|
||||||
|
child.parentElement = this
|
||||||
|
this.children.push(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttribute(name: string): string | null {
|
||||||
|
return this.attrs.get(name) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAttribute(name: string): boolean {
|
||||||
|
return this.attrs.has(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelector(selector: string): MockElement | null {
|
||||||
|
return this.querySelectorAll(selector)[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelectorAll(selector: string): MockElement[] {
|
||||||
|
const selectors = selector
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
const results: MockElement[] = []
|
||||||
|
const visit = (node: MockElement) => {
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (selectors.some((item) => matchesSelector(child, item))) {
|
||||||
|
results.push(child)
|
||||||
|
}
|
||||||
|
visit(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(this)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
getBoundingClientRect(): {
|
||||||
|
left: number
|
||||||
|
top: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
right: number
|
||||||
|
bottom: number
|
||||||
|
} {
|
||||||
|
let current: MockElement | null = this
|
||||||
|
while (current) {
|
||||||
|
if (current.style.display === 'none' || current.style.visibility === 'hidden') {
|
||||||
|
return {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...this.rect,
|
||||||
|
right: this.rect.left + this.rect.width,
|
||||||
|
bottom: this.rect.top + this.rect.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollIntoView(): void {}
|
||||||
|
|
||||||
|
focus(): void {}
|
||||||
|
|
||||||
|
dispatchEvent(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
click(): void {
|
||||||
|
this.onClick?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesSelector = (element: MockElement, selector: string): boolean => {
|
||||||
|
if (selector === '*') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (selector.includes(' ')) {
|
||||||
|
const parts = selector.split(/\s+/).filter(Boolean)
|
||||||
|
const last = parts.at(-1)
|
||||||
|
return last ? matchesSelector(element, last) : false
|
||||||
|
}
|
||||||
|
if (/^[a-z]+$/i.test(selector)) {
|
||||||
|
return element.tagName.toLowerCase() === selector.toLowerCase()
|
||||||
|
}
|
||||||
|
const tagMatch = selector.match(/^([a-z]+)(.+)$/i)
|
||||||
|
if (tagMatch) {
|
||||||
|
return (
|
||||||
|
element.tagName.toLowerCase() === tagMatch[1]?.toLowerCase() &&
|
||||||
|
matchesSelector(element, tagMatch[2] ?? '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const attrMatch = selector.match(/^\[([^=\]~*^$]+)([*^$]?=)?['"]?([^'"\]]*)['"]?\]$/)
|
||||||
|
if (attrMatch) {
|
||||||
|
const attr = attrMatch[1] ?? ''
|
||||||
|
const operator = attrMatch[2]
|
||||||
|
const expected = attrMatch[3] ?? ''
|
||||||
|
const value = element.getAttribute(attr)
|
||||||
|
if (!operator) {
|
||||||
|
return value !== null
|
||||||
|
}
|
||||||
|
if (operator === '*=') {
|
||||||
|
return value?.includes(expected) ?? false
|
||||||
|
}
|
||||||
|
if (operator === '^=') {
|
||||||
|
return value?.startsWith(expected) ?? false
|
||||||
|
}
|
||||||
|
return value === expected
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentRoot = makeElement({ rect: { left: 0, top: 0, width: 1280, height: 900 } })
|
||||||
|
const body = makeElement({ rect: { left: 0, top: 0, width: 1280, height: 900 } })
|
||||||
|
documentRoot.appendChild(body)
|
||||||
|
|
||||||
|
let dialog: MockElement | null = null
|
||||||
|
let currentHistory: MockElement | null = null
|
||||||
|
let nextHistory: MockElement | null = null
|
||||||
|
|
||||||
|
const confirmButton = makeElement({
|
||||||
|
tagName: 'button',
|
||||||
|
text: '删除',
|
||||||
|
attrs: { role: 'button' },
|
||||||
|
rect: { left: 680, top: 478, width: 80, height: 32 },
|
||||||
|
onClick: () => {
|
||||||
|
if (dialog) {
|
||||||
|
body.children = body.children.filter((child) => child !== dialog)
|
||||||
|
}
|
||||||
|
if (currentHistory) {
|
||||||
|
body.children = body.children.filter((child) => child !== currentHistory)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
dialog = makeElement({
|
||||||
|
tagName: 'div',
|
||||||
|
text: '确定删除对话 删除后不可恢复 取消 删除',
|
||||||
|
attrs: { role: 'dialog', 'aria-modal': 'true' },
|
||||||
|
rect: { left: 440, top: 320, width: 400, height: 220 },
|
||||||
|
visible: false,
|
||||||
|
children: [
|
||||||
|
makeElement({
|
||||||
|
tagName: 'button',
|
||||||
|
text: '取消',
|
||||||
|
attrs: { role: 'button' },
|
||||||
|
rect: { left: 580, top: 478, width: 80, height: 32 },
|
||||||
|
}),
|
||||||
|
confirmButton,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteItem = makeElement({
|
||||||
|
tagName: 'div',
|
||||||
|
text: '删除',
|
||||||
|
attrs: { role: 'menuitem' },
|
||||||
|
rect: { left: 240, top: 124, width: 96, height: 32 },
|
||||||
|
visible: false,
|
||||||
|
onClick: () => {
|
||||||
|
dialog!.style.display = 'block'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const menu = makeElement({
|
||||||
|
tagName: 'div',
|
||||||
|
text: '删除',
|
||||||
|
attrs: { role: 'menu' },
|
||||||
|
className: 'dropdown-menu',
|
||||||
|
rect: { left: 228, top: 112, width: 128, height: 48 },
|
||||||
|
children: [deleteItem],
|
||||||
|
})
|
||||||
|
|
||||||
|
const moreButton = makeElement({
|
||||||
|
tagName: 'button',
|
||||||
|
text: '',
|
||||||
|
attrs: { role: 'button' },
|
||||||
|
rect: { left: 252, top: 74, width: 28, height: 28 },
|
||||||
|
onClick: () => {
|
||||||
|
menu.style.display = 'block'
|
||||||
|
deleteItem.style.display = 'block'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
currentHistory = makeElement({
|
||||||
|
tagName: 'a',
|
||||||
|
text: '合肥全屋定制服务商选择指南',
|
||||||
|
attrs: { href: '/chat/38432306062349826', id: 'conversation_38432306062349826' },
|
||||||
|
rect: { left: 16, top: 64, width: 272, height: 48 },
|
||||||
|
className: 'chat-item',
|
||||||
|
children: [
|
||||||
|
makeElement({
|
||||||
|
tagName: 'span',
|
||||||
|
text: '合肥全屋定制服务商选择指南',
|
||||||
|
rect: { left: 24, top: 72, width: 210, height: 28 },
|
||||||
|
}),
|
||||||
|
makeElement({
|
||||||
|
tagName: 'div',
|
||||||
|
className: 'chat-item-menu-wrapper-DPvhf1',
|
||||||
|
rect: { left: 248, top: 72, width: 32, height: 28 },
|
||||||
|
children: [moreButton],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
nextHistory = makeElement({
|
||||||
|
tagName: 'a',
|
||||||
|
text: '合肥全屋定制公司推荐',
|
||||||
|
attrs: { href: '/chat/38432306010828802', id: 'conversation_38432306010828802' },
|
||||||
|
rect: { left: 16, top: 116, width: 272, height: 48 },
|
||||||
|
className: 'chat-item',
|
||||||
|
})
|
||||||
|
|
||||||
|
body.appendChild(currentHistory)
|
||||||
|
body.appendChild(nextHistory)
|
||||||
|
body.appendChild(menu)
|
||||||
|
body.appendChild(dialog)
|
||||||
|
|
||||||
|
const documentMock = {
|
||||||
|
body,
|
||||||
|
querySelectorAll: (selector: string) => body.querySelectorAll(selector),
|
||||||
|
querySelector: (selector: string) => body.querySelector(selector),
|
||||||
|
}
|
||||||
|
const windowMock = {
|
||||||
|
location: {
|
||||||
|
href: 'https://www.doubao.com/chat/',
|
||||||
|
origin: 'https://www.doubao.com',
|
||||||
|
},
|
||||||
|
innerWidth: 1280,
|
||||||
|
innerHeight: 900,
|
||||||
|
setTimeout: (callback: () => void) => {
|
||||||
|
callback()
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
getComputedStyle: (element: MockElement) => element.style,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mutableGlobal = globalThis as Record<string, unknown>
|
||||||
|
mutableGlobal.window = windowMock
|
||||||
|
mutableGlobal.document = documentMock
|
||||||
|
mutableGlobal.HTMLElement = MockElement
|
||||||
|
mutableGlobal.MouseEvent = MockMouseEvent
|
||||||
|
mutableGlobal.URL = URL
|
||||||
|
|
||||||
|
const result = await deleteDoubaoCurrentConversationInPage({
|
||||||
|
conversationId: null,
|
||||||
|
pageUrl: 'https://www.doubao.com/chat/',
|
||||||
|
title: '合肥全屋定制服务商怎么选',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
conversation_id: '38432306062349826',
|
||||||
|
menu_clicked: true,
|
||||||
|
delete_clicked: true,
|
||||||
|
confirm_clicked: true,
|
||||||
|
})
|
||||||
|
expect(body.children.includes(currentHistory)).toBe(false)
|
||||||
|
expect(body.children.includes(nextHistory)).toBe(true)
|
||||||
|
} finally {
|
||||||
|
const mutableGlobal = globalThis as Record<string, unknown>
|
||||||
|
mutableGlobal.window = previousWindow
|
||||||
|
mutableGlobal.document = previousDocument
|
||||||
|
mutableGlobal.HTMLElement = previousHTMLElement
|
||||||
|
mutableGlobal.MouseEvent = previousMouseEvent
|
||||||
|
mutableGlobal.URL = previousURL
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('filters byteimg and bytednsdoc asset CDN links from source items', () => {
|
it('filters byteimg and bytednsdoc asset CDN links from source items', () => {
|
||||||
expect(isNonCitationAssetDomain('p3-spider-image-sign.byteimg.com')).toBe(true)
|
expect(isNonCitationAssetDomain('p3-spider-image-sign.byteimg.com')).toBe(true)
|
||||||
expect(isNonCitationAssetDomain('lf3-static.bytednsdoc.com')).toBe(true)
|
expect(isNonCitationAssetDomain('lf3-static.bytednsdoc.com')).toBe(true)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user