添加自动运行广告条的样式和逻辑,并实现相关功能的测试
This commit is contained in:
@@ -652,6 +652,109 @@ header {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-bar {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 7px 46px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--amber) 24%, var(--border));
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: color-mix(in srgb, var(--amber) 10%, var(--bg-base));
|
||||||
|
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--bg-base) 75%, transparent);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-bar[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-badge {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
padding: 3px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--amber) 22%, var(--bg-base));
|
||||||
|
color: color-mix(in srgb, var(--amber) 80%, var(--text-primary));
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-viewport {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-track {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-text--clone {
|
||||||
|
display: none;
|
||||||
|
padding-left: var(--auto-run-ad-gap, 28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-link {
|
||||||
|
color: color-mix(in srgb, var(--amber) 80%, var(--link));
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
transition: color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-link:hover {
|
||||||
|
color: var(--link-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-link:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--amber) 36%, var(--border));
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-bar.is-scrolling .auto-run-ad-track {
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-width: max-content;
|
||||||
|
animation: auto-run-ad-marquee var(--auto-run-ad-duration, 16s) linear infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-bar.is-scrolling:hover .auto-run-ad-track,
|
||||||
|
.auto-run-ad-bar.is-scrolling:focus-within .auto-run-ad-track {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-run-ad-bar.is-scrolling .auto-run-ad-text--clone {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes auto-run-ad-marquee {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(calc(-1 * var(--auto-run-ad-scroll-distance, 0px)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.contribution-mode-panel {
|
.contribution-mode-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -124,6 +124,15 @@
|
|||||||
<option value="sub2api">SUB2API</option>
|
<option value="sub2api">SUB2API</option>
|
||||||
<option value="codex2api">Codex2API</option>
|
<option value="codex2api">Codex2API</option>
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="auto-run-ad-bar" class="auto-run-ad-bar" hidden aria-live="polite">
|
||||||
|
<span class="auto-run-ad-badge">广告</span>
|
||||||
|
<div id="auto-run-ad-viewport" class="auto-run-ad-viewport">
|
||||||
|
<div id="auto-run-ad-track" class="auto-run-ad-track">
|
||||||
|
<span id="auto-run-ad-text" class="auto-run-ad-text"></span>
|
||||||
|
<span id="auto-run-ad-text-clone" class="auto-run-ad-text auto-run-ad-text--clone" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="contribution-mode-panel" class="contribution-mode-panel" hidden>
|
<div id="contribution-mode-panel" class="contribution-mode-panel" hidden>
|
||||||
<div class="contribution-mode-panel-header">
|
<div class="contribution-mode-panel-header">
|
||||||
|
|||||||
+249
-4
@@ -66,6 +66,11 @@ const contributionUpdateLayer = document.getElementById('contribution-update-lay
|
|||||||
const contributionUpdateHint = document.getElementById('contribution-update-hint');
|
const contributionUpdateHint = document.getElementById('contribution-update-hint');
|
||||||
const contributionUpdateHintText = document.getElementById('contribution-update-hint-text');
|
const contributionUpdateHintText = document.getElementById('contribution-update-hint-text');
|
||||||
const btnDismissContributionUpdateHint = document.getElementById('btn-dismiss-contribution-update-hint');
|
const btnDismissContributionUpdateHint = document.getElementById('btn-dismiss-contribution-update-hint');
|
||||||
|
const autoRunAdBar = document.getElementById('auto-run-ad-bar');
|
||||||
|
const autoRunAdViewport = document.getElementById('auto-run-ad-viewport');
|
||||||
|
const autoRunAdTrack = document.getElementById('auto-run-ad-track');
|
||||||
|
const autoRunAdText = document.getElementById('auto-run-ad-text');
|
||||||
|
const autoRunAdTextClone = document.getElementById('auto-run-ad-text-clone');
|
||||||
const stepsProgress = document.getElementById('steps-progress');
|
const stepsProgress = document.getElementById('steps-progress');
|
||||||
const btnAutoRun = document.getElementById('btn-auto-run');
|
const btnAutoRun = document.getElementById('btn-auto-run');
|
||||||
const btnAutoContinue = document.getElementById('btn-auto-continue');
|
const btnAutoContinue = document.getElementById('btn-auto-continue');
|
||||||
@@ -1346,6 +1351,7 @@ let configActionInFlight = false;
|
|||||||
let currentReleaseSnapshot = null;
|
let currentReleaseSnapshot = null;
|
||||||
let currentContributionContentSnapshot = null;
|
let currentContributionContentSnapshot = null;
|
||||||
let contributionContentSnapshotRequestInFlight = null;
|
let contributionContentSnapshotRequestInFlight = null;
|
||||||
|
let autoRunAdScrollSyncFrame = 0;
|
||||||
|
|
||||||
function normalizeAutomationWindowId(value) {
|
function normalizeAutomationWindowId(value) {
|
||||||
if (value === null || value === undefined || value === '') {
|
if (value === null || value === undefined || value === '') {
|
||||||
@@ -11013,6 +11019,156 @@ function getContributionUpdatePromptLines(snapshot = currentContributionContentS
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAutoRunAdConfig(snapshot = currentContributionContentSnapshot) {
|
||||||
|
const items = Array.isArray(snapshot?.items) ? snapshot.items : [];
|
||||||
|
const adItem = items.find((item) =>
|
||||||
|
item
|
||||||
|
&& String(item.slug || '').trim().toLowerCase() === 'extension_auto_run_ad'
|
||||||
|
);
|
||||||
|
if (!adItem || !adItem.isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = String(adItem.text || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
title: String(adItem.title || '').trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeAutoRunAdUrl(value = '') {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(raw);
|
||||||
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return parsed.href;
|
||||||
|
} catch (_error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAutoRunAdSegments(value = '') {
|
||||||
|
const rawText = String(value || '').trim();
|
||||||
|
if (!rawText) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
let cursor = 0;
|
||||||
|
|
||||||
|
while (cursor < rawText.length) {
|
||||||
|
const labelStart = rawText.indexOf('[', cursor);
|
||||||
|
if (labelStart < 0) {
|
||||||
|
segments.push({
|
||||||
|
type: 'text',
|
||||||
|
text: rawText.slice(cursor),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelEnd = rawText.indexOf(']', labelStart + 1);
|
||||||
|
if (labelEnd < 0 || rawText[labelEnd + 1] !== '(') {
|
||||||
|
segments.push({
|
||||||
|
type: 'text',
|
||||||
|
text: rawText.slice(cursor),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelStart > cursor) {
|
||||||
|
segments.push({
|
||||||
|
type: 'text',
|
||||||
|
text: rawText.slice(cursor, labelStart),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlEnd = labelEnd + 2;
|
||||||
|
let depth = 1;
|
||||||
|
while (urlEnd < rawText.length && depth > 0) {
|
||||||
|
const ch = rawText[urlEnd];
|
||||||
|
if (ch === '(') {
|
||||||
|
depth += 1;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
depth -= 1;
|
||||||
|
}
|
||||||
|
urlEnd += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth > 0) {
|
||||||
|
segments.push({
|
||||||
|
type: 'text',
|
||||||
|
text: rawText.slice(labelStart),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = rawText.slice(labelStart + 1, labelEnd).trim();
|
||||||
|
const rawSegment = rawText.slice(labelStart, urlEnd);
|
||||||
|
const url = sanitizeAutoRunAdUrl(rawText.slice(labelEnd + 2, urlEnd - 1));
|
||||||
|
if (label && url) {
|
||||||
|
segments.push({
|
||||||
|
type: 'link',
|
||||||
|
text: label,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
segments.push({
|
||||||
|
type: 'text',
|
||||||
|
text: rawSegment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cursor = urlEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.filter((segment) => String(segment?.text || '').length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoRunAdPlainText(segments = []) {
|
||||||
|
return segments.map((segment) => String(segment?.text || '')).join('').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAutoRunAdSegments(container, segments = [], options = {}) {
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tabIndex = undefined } = options;
|
||||||
|
container.textContent = '';
|
||||||
|
for (const segment of Array.isArray(segments) ? segments : []) {
|
||||||
|
const text = String(segment?.text || '');
|
||||||
|
if (!text) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment?.type === 'link' && segment?.url) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.className = 'auto-run-ad-link';
|
||||||
|
link.href = segment.url;
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
link.dataset.externalUrl = segment.url;
|
||||||
|
if (Number.isInteger(tabIndex)) {
|
||||||
|
link.tabIndex = tabIndex;
|
||||||
|
}
|
||||||
|
link.textContent = text;
|
||||||
|
container.appendChild(link);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(document.createTextNode(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function positionContributionUpdateHint() {
|
function positionContributionUpdateHint() {
|
||||||
if (!contributionUpdateLayer || !contributionUpdateHint || !btnContributionMode) {
|
if (!contributionUpdateLayer || !contributionUpdateHint || !btnContributionMode) {
|
||||||
return;
|
return;
|
||||||
@@ -11086,6 +11242,86 @@ function renderContributionUpdateHint(snapshot = currentContributionContentSnaps
|
|||||||
positionContributionUpdateHint();
|
positionContributionUpdateHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetAutoRunAdScrollState() {
|
||||||
|
if (!autoRunAdBar || !autoRunAdTrack || !autoRunAdTextClone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
autoRunAdBar.classList.remove('is-scrolling');
|
||||||
|
autoRunAdBar.style.removeProperty('--auto-run-ad-gap');
|
||||||
|
autoRunAdBar.style.removeProperty('--auto-run-ad-duration');
|
||||||
|
autoRunAdBar.style.removeProperty('--auto-run-ad-scroll-distance');
|
||||||
|
autoRunAdTextClone.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAutoRunAdScrollState() {
|
||||||
|
if (!autoRunAdBar || !autoRunAdViewport || !autoRunAdTrack || !autoRunAdText || !autoRunAdTextClone || autoRunAdBar.hidden) {
|
||||||
|
resetAutoRunAdScrollState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = String(autoRunAdText.textContent || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
resetAutoRunAdScrollState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportWidth = autoRunAdViewport.clientWidth || 0;
|
||||||
|
const textWidth = Math.ceil(autoRunAdText.getBoundingClientRect().width || 0);
|
||||||
|
if (!viewportWidth || !textWidth || textWidth <= viewportWidth) {
|
||||||
|
resetAutoRunAdScrollState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollGap = 32;
|
||||||
|
const scrollDistance = textWidth + scrollGap;
|
||||||
|
const durationSeconds = Math.max(18, scrollDistance / 24);
|
||||||
|
autoRunAdBar.classList.add('is-scrolling');
|
||||||
|
autoRunAdBar.style.setProperty('--auto-run-ad-gap', `${scrollGap}px`);
|
||||||
|
autoRunAdBar.style.setProperty('--auto-run-ad-duration', `${durationSeconds.toFixed(2)}s`);
|
||||||
|
autoRunAdBar.style.setProperty('--auto-run-ad-scroll-distance', `${scrollDistance}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleAutoRunAdScrollSync() {
|
||||||
|
if (autoRunAdScrollSyncFrame && typeof window.cancelAnimationFrame === 'function') {
|
||||||
|
window.cancelAnimationFrame(autoRunAdScrollSyncFrame);
|
||||||
|
autoRunAdScrollSyncFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.requestAnimationFrame === 'function') {
|
||||||
|
autoRunAdScrollSyncFrame = window.requestAnimationFrame(() => {
|
||||||
|
autoRunAdScrollSyncFrame = 0;
|
||||||
|
syncAutoRunAdScrollState();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncAutoRunAdScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAutoRunAd(snapshot = currentContributionContentSnapshot) {
|
||||||
|
if (!autoRunAdBar || !autoRunAdText || !autoRunAdTextClone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getAutoRunAdConfig(snapshot);
|
||||||
|
const visible = Boolean(config);
|
||||||
|
autoRunAdBar.hidden = !visible;
|
||||||
|
if (!visible) {
|
||||||
|
autoRunAdBar.title = '';
|
||||||
|
autoRunAdText.textContent = '';
|
||||||
|
autoRunAdTextClone.textContent = '';
|
||||||
|
resetAutoRunAdScrollState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = parseAutoRunAdSegments(config.text);
|
||||||
|
const plainText = getAutoRunAdPlainText(segments);
|
||||||
|
autoRunAdBar.title = plainText || config.text;
|
||||||
|
renderAutoRunAdSegments(autoRunAdText, segments);
|
||||||
|
renderAutoRunAdSegments(autoRunAdTextClone, segments, { tabIndex: -1 });
|
||||||
|
scheduleAutoRunAdScrollSync();
|
||||||
|
}
|
||||||
|
|
||||||
function dismissContributionUpdateHint() {
|
function dismissContributionUpdateHint() {
|
||||||
const promptVersion = String(currentContributionContentSnapshot?.promptVersion || '').trim();
|
const promptVersion = String(currentContributionContentSnapshot?.promptVersion || '').trim();
|
||||||
if (promptVersion) {
|
if (promptVersion) {
|
||||||
@@ -11098,6 +11334,7 @@ async function refreshContributionContentHint() {
|
|||||||
if (!contributionContentService?.getContentUpdateSnapshot) {
|
if (!contributionContentService?.getContentUpdateSnapshot) {
|
||||||
currentContributionContentSnapshot = null;
|
currentContributionContentSnapshot = null;
|
||||||
renderContributionUpdateHint();
|
renderContributionUpdateHint();
|
||||||
|
renderAutoRunAd();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (contributionContentSnapshotRequestInFlight) {
|
if (contributionContentSnapshotRequestInFlight) {
|
||||||
@@ -11108,11 +11345,13 @@ async function refreshContributionContentHint() {
|
|||||||
.then((snapshot) => {
|
.then((snapshot) => {
|
||||||
currentContributionContentSnapshot = snapshot;
|
currentContributionContentSnapshot = snapshot;
|
||||||
renderContributionUpdateHint(snapshot);
|
renderContributionUpdateHint(snapshot);
|
||||||
|
renderAutoRunAd(snapshot);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
currentContributionContentSnapshot = null;
|
currentContributionContentSnapshot = null;
|
||||||
renderContributionUpdateHint(null);
|
renderContributionUpdateHint(null);
|
||||||
|
renderAutoRunAd(null);
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -13631,6 +13870,12 @@ autoStartMessage?.addEventListener('click', (event) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openExternalUrl(link.dataset.externalUrl || link.href);
|
openExternalUrl(link.dataset.externalUrl || link.href);
|
||||||
});
|
});
|
||||||
|
autoRunAdBar?.addEventListener('click', (event) => {
|
||||||
|
const link = event.target?.closest?.('a[data-external-url]');
|
||||||
|
if (!link) return;
|
||||||
|
event.preventDefault();
|
||||||
|
openExternalUrl(link.dataset.externalUrl || link.href);
|
||||||
|
});
|
||||||
btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
|
btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
|
||||||
|
|
||||||
async function startAutoRunFromCurrentSettings() {
|
async function startAutoRunFromCurrentSettings() {
|
||||||
@@ -13642,11 +13887,10 @@ async function startAutoRunFromCurrentSettings() {
|
|||||||
: getRunCountValue();
|
: getRunCountValue();
|
||||||
registerPendingAutoRunStartRunCount(requestedTotalRuns);
|
registerPendingAutoRunStartRunCount(requestedTotalRuns);
|
||||||
|
|
||||||
try {
|
// 站点内容刷新只影响提示/广告展示,不应阻塞自动流程启动。
|
||||||
await refreshContributionContentHint();
|
refreshContributionContentHint().catch((error) => {
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to refresh contribution content hint before auto run:', error);
|
console.warn('Failed to refresh contribution content hint before auto run:', error);
|
||||||
}
|
});
|
||||||
|
|
||||||
if (typeof persistCurrentSettingsForAction === 'function') {
|
if (typeof persistCurrentSettingsForAction === 'function') {
|
||||||
await persistCurrentSettingsForAction();
|
await persistCurrentSettingsForAction();
|
||||||
@@ -16892,6 +17136,7 @@ document.addEventListener('keydown', (event) => {
|
|||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
positionContributionUpdateHint();
|
positionContributionUpdateHint();
|
||||||
|
scheduleAutoRunAdScrollSync();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('scroll', () => {
|
document.addEventListener('scroll', () => {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => sidepanelSource.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let i = start; i < sidepanelSource.length; i += 1) {
|
||||||
|
const ch = sidepanelSource[i];
|
||||||
|
if (ch === '(') {
|
||||||
|
parenDepth += 1;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
signatureEnded = true;
|
||||||
|
}
|
||||||
|
} else if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < sidepanelSource.length; end += 1) {
|
||||||
|
const ch = sidepanelSource[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sidepanelSource.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('sanitizeAutoRunAdUrl'),
|
||||||
|
extractFunction('parseAutoRunAdSegments'),
|
||||||
|
extractFunction('getAutoRunAdPlainText'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
${bundle}
|
||||||
|
return {
|
||||||
|
sanitizeAutoRunAdUrl,
|
||||||
|
parseAutoRunAdSegments,
|
||||||
|
getAutoRunAdPlainText,
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
test('sanitizeAutoRunAdUrl only allows http and https links', () => {
|
||||||
|
assert.equal(api.sanitizeAutoRunAdUrl('https://example.com/docs'), 'https://example.com/docs');
|
||||||
|
assert.equal(api.sanitizeAutoRunAdUrl('http://example.com/docs'), 'http://example.com/docs');
|
||||||
|
assert.equal(api.sanitizeAutoRunAdUrl('javascript:alert(1)'), '');
|
||||||
|
assert.equal(api.sanitizeAutoRunAdUrl('data:text/html,hello'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseAutoRunAdSegments converts markdown links into safe link segments', () => {
|
||||||
|
const segments = api.parseAutoRunAdSegments('查看 [教程](https://example.com/tutorial) 和 [频道](https://example.com/channel)');
|
||||||
|
|
||||||
|
assert.deepEqual(segments, [
|
||||||
|
{ type: 'text', text: '查看 ' },
|
||||||
|
{ type: 'link', text: '教程', url: 'https://example.com/tutorial' },
|
||||||
|
{ type: 'text', text: ' 和 ' },
|
||||||
|
{ type: 'link', text: '频道', url: 'https://example.com/channel' },
|
||||||
|
]);
|
||||||
|
assert.equal(api.getAutoRunAdPlainText(segments), '查看 教程 和 频道');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseAutoRunAdSegments keeps unsafe markdown links as plain text', () => {
|
||||||
|
const segments = api.parseAutoRunAdSegments('危险链接 [不要点](javascript:alert(1))');
|
||||||
|
|
||||||
|
assert.deepEqual(segments, [
|
||||||
|
{ type: 'text', text: '危险链接 ' },
|
||||||
|
{ type: 'text', text: '[不要点](javascript:alert(1))' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -151,7 +151,7 @@ return {
|
|||||||
`)();
|
`)();
|
||||||
}
|
}
|
||||||
|
|
||||||
test('startAutoRunFromCurrentSettings refreshes contribution content hint before starting auto run', async () => {
|
test('startAutoRunFromCurrentSettings kicks off contribution content refresh without blocking auto run', async () => {
|
||||||
const api = createApi();
|
const api = createApi();
|
||||||
|
|
||||||
const result = await api.startAutoRunFromCurrentSettings();
|
const result = await api.startAutoRunFromCurrentSettings();
|
||||||
@@ -175,10 +175,24 @@ test('startAutoRunFromCurrentSettings continues auto run when contribution conte
|
|||||||
assert.equal(result, true);
|
assert.equal(result, true);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
events.map((entry) => entry.type),
|
events.map((entry) => entry.type),
|
||||||
['refresh', 'warn', 'sync-settings', 'send']
|
['refresh', 'sync-settings', 'send', 'warn']
|
||||||
|
);
|
||||||
|
assert.equal(events[2].message.type, 'AUTO_RUN');
|
||||||
|
assert.match(String(events[3].args[0]), /Failed to refresh contribution content hint before auto run/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startAutoRunFromCurrentSettings does not wait for a hanging contribution content refresh', async () => {
|
||||||
|
const api = createApi({
|
||||||
|
refreshImpl: '() => new Promise(() => {})',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.startAutoRunFromCurrentSettings();
|
||||||
|
|
||||||
|
assert.equal(result, true);
|
||||||
|
assert.deepEqual(
|
||||||
|
api.getEvents().map((entry) => entry.type),
|
||||||
|
['refresh', 'sync-settings', 'send']
|
||||||
);
|
);
|
||||||
assert.match(String(events[1].args[0]), /Failed to refresh contribution content hint before auto run/);
|
|
||||||
assert.equal(events[3].message.type, 'AUTO_RUN');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('startAutoRunFromCurrentSettings does not block auto run when contribution content has updates', async () => {
|
test('startAutoRunFromCurrentSettings does not block auto run when contribution content has updates', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user