(enabled: boolean, calculate
}, [])
return { ref: refCallback, element: ref, keepBottom }
+}
+
+
+
+
+/**
+ * Custom hook that queries the DOM for an element matching the given selector.
+ * Optionally, it can remount and re-query the DOM at a specified interval.
+ *
+ * @param {string} selector - The CSS selector to query the DOM.
+ * @param {boolean} [remount=false] - If true, the hook will re-query the DOM at the specified interval.
+ * @returns {Element | null} - The DOM element matching the selector, or null if no element is found.
+ *
+ * @example
+ * // Usage in a React component
+ * const MyComponent = () => {
+ * const element = useQuerySelector('#my-element', true);
+ *
+ * useEffect(() => {
+ * if (element) {
+ * console.log('Element found:', element);
+ * }
+ * }, [element]);
+ *
+ * return Check the console for the element.
;
+ * };
+ */
+export function useQuerySelector(selector: string, remount: boolean = false): E | null {
+
+ const [element, setElement] = useState(document.querySelector(selector))
+
+ useInterval(() => {
+ const el = document.querySelector(selector)
+ if (el) {
+ setElement(el)
+ }
+ }, 500, { paused: !remount && !!element, immediate: true })
+
+ return element as E
}
\ No newline at end of file
diff --git a/src/hooks/styles.ts b/src/hooks/styles.ts
index b63b3425..b8e65e82 100644
--- a/src/hooks/styles.ts
+++ b/src/hooks/styles.ts
@@ -2,7 +2,7 @@ import { useMemo } from "react";
export function useComputedStyle(element: Element): CSSStyleDeclaration {
- return useMemo(() => window.getComputedStyle(element), [element]);
+ return useMemo(() => element ? window.getComputedStyle(element) : {} as CSSStyleDeclaration, [element]);
}
export function useContrast(background: Element) {
diff --git a/src/options/index.tsx b/src/options/index.tsx
index 216c3a0e..8aa33e2c 100644
--- a/src/options/index.tsx
+++ b/src/options/index.tsx
@@ -105,6 +105,9 @@ function SettingPage(): JSX.Element {
if (!(settings instanceof Object)) {
throw new Error('导入的设定文件格式错误。')
}
+ if (Object.keys(settings).length === 0) {
+ throw new Error('导入的设定文件格式错误。')
+ }
if (!Object.keys(settings).every(key => (fragmentKeys as string[]).includes(key))) {
throw new Error('导入的设定文件格式错误。')
}
@@ -117,8 +120,7 @@ function SettingPage(): JSX.Element {
})();
toast.promise(importing, {
loading: '正在导入设定...',
- success: '设定已经导入成功。',
- error: err => '导入设定失败: ' + err.message
+ success: '设定已经导入成功。'
})
await importing
if (!processing) {
diff --git a/tests/helpers/bilibili-api.ts b/tests/helpers/bilibili-api.ts
index 2466dd67..3f488729 100644
--- a/tests/helpers/bilibili-api.ts
+++ b/tests/helpers/bilibili-api.ts
@@ -1,7 +1,7 @@
import { request, type APIRequestContext } from "@playwright/test";
-import { sendInternal } from "~background/messages";
+import { Mutex } from 'async-mutex';
import type { StreamUrls } from "~background/messages/get-stream-urls";
-import type { V1Response, StreamUrlResponse } from "~types/bilibili";
+import type { StreamUrlResponse, V1Response } from "~types/bilibili";
import logger from "./logger";
export interface LiveRoomInfo {
@@ -35,6 +35,8 @@ export default class BilbiliApi {
return new BilbiliApi(context)
}
+ private readonly mutex = new Mutex()
+
/**
* 构造BilbiliApi的新实例。
* @param context - API请求的上下文。
@@ -48,9 +50,14 @@ export default class BilbiliApi {
* @throws 如果获取操作失败,则抛出错误。
*/
private async fetch(path: string): Promise {
- const res = await this.context.get(path)
- if (!res.ok()) throw new Error(`获取bilibili API失败:${res.statusText()}`)
- return await res.json()
+ const release = await this.mutex.acquire()
+ try {
+ const res = await this.context.get(path)
+ if (!res.ok()) throw new Error(`获取bilibili API失败:${res.statusText()}`)
+ return await res.json()
+ } finally {
+ release()
+ }
}
/**
diff --git a/tests/pages/options.spec.ts b/tests/pages/options.spec.ts
index aa895435..8024fd50 100644
--- a/tests/pages/options.spec.ts
+++ b/tests/pages/options.spec.ts
@@ -4,6 +4,7 @@ import { expect, test } from '@tests/fixtures/background'
import BilibiliPage from '@tests/helpers/bilibili-page'
import logger from '@tests/helpers/logger'
import { getSuperChatList } from '@tests/utils/playwright'
+import { mkdir, writeFile } from 'fs/promises'
import type { MV2Settings } from '~migrations/schema'
test.beforeEach(async ({ page, extensionId }) => {
@@ -146,13 +147,45 @@ test('測試導出導入設定', async ({ page }) => {
await expect(inputLineGap).toHaveValue('7')
})
+// 向下兼容,即舊版設定檔沒有某些新設定區塊,依然可以導入
+test('測試導入向下兼容設定', async ({ page }) => {
+ await mkdir('out', { recursive: true })
+
+ {
+ logger.info('正在嘗試導入空設定....')
+ await writeFile('out/empty.json', '{}')
+
+ const fileChoosing = page.waitForEvent('filechooser')
+ await page.getByText('导入设定').click()
+ const fileChooser = await fileChoosing
+ await fileChooser.setFiles('out/empty.json')
+
+ await page.getByText('导入的设定文件格式错误。').waitFor({ state: 'visible' })
+ }
+
+ await page.reload({ waitUntil: 'domcontentloaded' })
+
+ {
+ logger.info('正在嘗試導入正確設定....')
+ await writeFile('out/valid.json', JSON.stringify({ 'settings.version': {} }))
+
+ const fileChoosing = page.waitForEvent('filechooser')
+ await page.getByText('导入设定').click()
+ const fileChooser = await fileChoosing
+ await fileChooser.setFiles('out/valid.json')
+
+ await page.getByText('设定已经导入成功。').waitFor({ state: 'visible' })
+ }
+
+})
+
test('測試清空數據庫', async ({ page, front: room, api }) => {
await page.bringToFront()
const feature = page.getByText('功能设定')
await feature.click()
-
+
const btns = await page.locator('section#settings\\.features').getByText('启用离线记录').all()
for (const btn of btns) {
await btn.click()
@@ -517,7 +550,7 @@ test('測試导航', async ({ page, serviceWorker }) => {
test('測試點擊使用指南', async ({ context, page }) => {
await page.getByText('功能设定').click()
-
+
const tutorial = context.waitForEvent('page')
await page.getByText('使用指南').click()
const tutorialPage = await tutorial