对接
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
export async function copyLiveText(value, {
|
||||
clipboard = navigator.clipboard,
|
||||
documentRef = document,
|
||||
} = {}) {
|
||||
const text = String(value)
|
||||
if (clipboard?.writeText) {
|
||||
try { await clipboard.writeText(text); return }
|
||||
catch { /* Older or restricted browsers can still support a selection copy. */ }
|
||||
}
|
||||
const previousFocus = documentRef.activeElement
|
||||
const input = documentRef.createElement('textarea')
|
||||
input.value = text
|
||||
input.readOnly = true
|
||||
input.style.position = 'fixed'
|
||||
input.style.left = '-9999px'
|
||||
documentRef.body.appendChild(input)
|
||||
try {
|
||||
input.select()
|
||||
if (!documentRef.execCommand('copy')) throw new Error('Clipboard copy failed')
|
||||
} finally {
|
||||
input.remove()
|
||||
previousFocus?.focus?.({ preventScroll: true })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { copyLiveText } from './clipboard.js'
|
||||
|
||||
function legacyDocument(success) {
|
||||
const input = { style: {}, select() { this.selected = true }, remove() { this.removed = true } }
|
||||
return {
|
||||
input,
|
||||
activeElement: { focus() {} },
|
||||
createElement: () => input,
|
||||
body: { appendChild() {} },
|
||||
execCommand: () => success,
|
||||
}
|
||||
}
|
||||
|
||||
test('copies an absolute link through the modern clipboard', async () => {
|
||||
let copied
|
||||
await copyLiveText('https://live.example.com/#/studio/2', {
|
||||
clipboard: { async writeText(value) { copied = value } },
|
||||
documentRef: {},
|
||||
})
|
||||
assert.equal(copied, 'https://live.example.com/#/studio/2')
|
||||
})
|
||||
|
||||
test('uses a selection fallback when clipboard access is denied', async () => {
|
||||
const documentRef = legacyDocument(true)
|
||||
await copyLiveText('viewer-link', {
|
||||
clipboard: { async writeText() { throw new Error('denied') } },
|
||||
documentRef,
|
||||
})
|
||||
assert.equal(documentRef.input.value, 'viewer-link')
|
||||
assert.equal(documentRef.input.selected, true)
|
||||
assert.equal(documentRef.input.removed, true)
|
||||
})
|
||||
|
||||
test('does not report success when both browser copy mechanisms fail', async () => {
|
||||
const documentRef = legacyDocument(false)
|
||||
await assert.rejects(copyLiveText('viewer-link', { clipboard: null, documentRef }), /copy failed/)
|
||||
assert.equal(documentRef.input.removed, true)
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
export function getRoomLinks(roomId, {
|
||||
origin = window.location.origin,
|
||||
clientBaseUrl = import.meta.env?.VITE_LIVE_CLIENT_URL,
|
||||
development = import.meta.env?.DEV,
|
||||
} = {}) {
|
||||
const id = Number(roomId)
|
||||
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('Invalid live room ID')
|
||||
|
||||
const clientBase = String(clientBaseUrl || '').trim()
|
||||
const url = new URL(clientBase || '/', origin)
|
||||
if (!clientBase && development) url.port = '5173'
|
||||
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
|
||||
throw new Error('Invalid live client URL')
|
||||
}
|
||||
url.search = ''
|
||||
if (!/\.html?$/i.test(url.pathname)) url.pathname = `${url.pathname.replace(/\/+$/, '')}/`
|
||||
url.hash = `/live/${id}`
|
||||
const viewer = url.toString()
|
||||
url.hash = `/studio/${id}`
|
||||
return { viewer, studio: url.toString() }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { getRoomLinks } from './roomLinks.js'
|
||||
|
||||
test('production links target the PC routes, not the administration path', () => {
|
||||
assert.deepEqual(getRoomLinks(2, { origin: 'https://live.example.com', development: false }), {
|
||||
viewer: 'https://live.example.com/#/live/2',
|
||||
studio: 'https://live.example.com/#/studio/2',
|
||||
})
|
||||
})
|
||||
|
||||
test('custom client origins and base paths discard unrelated query and hash state', () => {
|
||||
assert.deepEqual(getRoomLinks('12', {
|
||||
origin: 'https://admin.example.com',
|
||||
clientBaseUrl: 'https://client.example.com/player?token=unused#/old',
|
||||
}), {
|
||||
viewer: 'https://client.example.com/player/#/live/12',
|
||||
studio: 'https://client.example.com/player/#/studio/12',
|
||||
})
|
||||
})
|
||||
|
||||
test('development links use the PC dev server and support an explicit port override', () => {
|
||||
assert.equal(getRoomLinks(1, { origin: 'http://localhost:8080', development: true }).viewer, 'http://localhost:5173/#/live/1')
|
||||
assert.equal(getRoomLinks(1, {
|
||||
origin: 'http://localhost:8080',
|
||||
development: true,
|
||||
clientBaseUrl: 'http://localhost:5174/',
|
||||
}).studio, 'http://localhost:5174/#/studio/1')
|
||||
})
|
||||
|
||||
test('client base supports a relative path and a static HTML entrypoint', () => {
|
||||
assert.equal(getRoomLinks(3, {
|
||||
origin: 'https://live.example.com',
|
||||
clientBaseUrl: '/pc/index.html',
|
||||
}).viewer, 'https://live.example.com/pc/index.html#/live/3')
|
||||
})
|
||||
|
||||
test('invalid room IDs, executable schemes and URL credentials are rejected', () => {
|
||||
for (const id of [0, -1, 1.2, '2/other', undefined]) {
|
||||
assert.throws(() => getRoomLinks(id, { origin: 'https://live.example.com' }))
|
||||
}
|
||||
for (const clientBaseUrl of ['javascript:alert(1)', 'https://user:password@example.com/']) {
|
||||
assert.throws(() => getRoomLinks(2, { origin: 'https://live.example.com', clientBaseUrl }))
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user