diff --git a/.gitignore b/.gitignore
index 62c1b09..65ceac2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
.worktrees/
.old-reference/
+.local/
dist/
+dist-release/
+release/
node_modules/
# Local debug captures
diff --git a/README.md b/README.md
index 0b86272..512e9e4 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,18 @@ npm test
npm run build
```
+## Release Build
+
+```bash
+npm run build:release
+npm run package:internal
+```
+
+- `npm run build` outputs the development bundle to `dist/`
+- `npm run build:release` outputs the internal distribution bundle to `dist-release/`
+- `npm run package:internal` creates `release/star-chart-search-enhancer-internal.zip`
+- The extension ID is fixed to `pkjopdibdnomhogjheclhnknmejccffg`
+
## Load The Extension
1. Run `npm run build`
@@ -37,7 +49,7 @@ Replace these before real sign-in testing:
- `apiResource`
- Any extra scopes beyond `openid`, `profile`, and `offline_access`
-The popup dev panel is controlled by `enableDevAuthPanel`.
+The popup dev panel is controlled by `enableDevAuthPanel` and is disabled by default.
## Popup Behavior
@@ -66,7 +78,7 @@ The popup dev panel is controlled by `enableDevAuthPanel`.
6. Click `提交批次`
7. Enter a batch name in the browser prompt
8. Confirm the toolbar shows `批次提交成功`
-9. Confirm the mock batch response accepts the payload and reports the submitted `batchId`
+9. Confirm the mock batch response accepts the payload
## Market Auth Gate
@@ -91,7 +103,6 @@ When the market page is opened without a valid auth state, the content script re
"creatorName": "王少卿",
"resource": "https://talent-search.intelligrow.cn",
"batchName": "自动验证批次",
- "batchId": "p7pdhhtde8kj-<提交当时的 ISO 时间戳>",
"createdAt": "<提交当时的 ISO 时间戳>",
"authors": [
{ "authorId": "7041184989643276324", "authorName": "旖旖小虎🐯" },
@@ -116,4 +127,7 @@ When the market page is opened without a valid auth state, the content script re
{ "authorId": "7319160797236559882", "authorName": "郑皓文" }
]
}
-```
\ No newline at end of file
+```
+
+Internal distribution steps are documented in
+`docs/internal-extension-distribution.md`.
diff --git a/docs/internal-extension-distribution.md b/docs/internal-extension-distribution.md
new file mode 100644
index 0000000..6d1cf4f
--- /dev/null
+++ b/docs/internal-extension-distribution.md
@@ -0,0 +1,31 @@
+# Internal Extension Distribution
+
+## Fixed Extension ID
+
+- Manifest key is fixed in the project.
+- The unpacked extension ID is `pkjopdibdnomhogjheclhnknmejccffg`.
+- Update Logto with:
+ - `https://pkjopdibdnomhogjheclhnknmejccffg.chromiumapp.org/callback`
+ - `https://pkjopdibdnomhogjheclhnknmejccffg.chromiumapp.org/`
+ - `chrome-extension://pkjopdibdnomhogjheclhnknmejccffg`
+
+## Internal Package
+
+1. Run `npm test`.
+2. Run `npm run package:internal`.
+3. Send `release/star-chart-search-enhancer-internal.zip` to coworkers.
+
+## Coworker Install Steps
+
+1. Unzip `star-chart-search-enhancer-internal.zip`.
+2. Open `chrome://extensions`.
+3. Enable developer mode.
+4. Click `Load unpacked`.
+5. Select the unzipped folder.
+6. Confirm the extension ID is `pkjopdibdnomhogjheclhnknmejccffg`.
+
+## Notes
+
+- Keep `.local/extension-key.pem` private and backed up internally.
+- Do not commit or share the private key with people who only need to install the extension.
+- If the batch submit backend changes away from `192.168.31.21:8083`, update `scripts/manifest.mjs` before packaging.
diff --git a/package.json b/package.json
index 8b95f4d..527b6db 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,10 @@
"private": true,
"scripts": {
"build": "node scripts/build.mjs",
+ "build:release": "BUILD_TARGET=release node scripts/build.mjs",
"mock:protected-api": "node scripts/mock-protected-api.mjs",
+ "package:internal": "npm run build:release && node scripts/package-release.mjs",
+ "package:release": "npm run build:release && node scripts/package-release.mjs",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest --passWithNoTests"
},
diff --git a/scripts/build.mjs b/scripts/build.mjs
index d6179b1..c6d01af 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -1,12 +1,17 @@
-import { cp, mkdir, rm } from "node:fs/promises";
+import { cp, mkdir, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "tsup";
+import { createManifest } from "./manifest.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
-const distDir = path.join(projectRoot, "dist");
+const buildTarget = process.env.BUILD_TARGET === "release" ? "release" : "development";
+const distDir = path.join(
+ projectRoot,
+ buildTarget === "release" ? "dist-release" : "dist"
+);
await rm(distDir, { recursive: true, force: true });
await mkdir(path.join(distDir, "content"), { recursive: true });
@@ -68,11 +73,16 @@ await build({
}
});
-await cp(
- path.join(projectRoot, "src/manifest.json"),
- path.join(distDir, "manifest.json")
+await writeFile(
+ path.join(distDir, "manifest.json"),
+ `${JSON.stringify(createManifest({ target: buildTarget }), null, 2)}\n`
);
await cp(
path.join(projectRoot, "src/popup/index.html"),
path.join(distDir, "popup/index.html")
);
+await cp(
+ path.join(projectRoot, "src/assets"),
+ path.join(distDir, "assets"),
+ { recursive: true }
+);
diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs
new file mode 100644
index 0000000..1de02bf
--- /dev/null
+++ b/scripts/manifest.mjs
@@ -0,0 +1,76 @@
+const sharedIcons = {
+ 16: "assets/icons/icon-16.png",
+ 32: "assets/icons/icon-32.png",
+ 48: "assets/icons/icon-48.png",
+ 128: "assets/icons/icon-128.png"
+};
+const extensionKey =
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CaZJxcX97TbRXCR08L10t9EZFV31+wPnUgDf21j2f0qYaWdblzWXfVkeU9jGb2Hr2Etpp7F/XuBa6pcipUXkzMMBkJ42KOkciAwbuzTBoAtGB8o9aoWigtax+gGfSz+T3BjqxKBJtXqeqbIAKCDIlxRKIrY+KcY1Z+mD5BKcBHKsUDQPlHsrjc1g0wIBD5doz9LoOk1Wso6gK5cSeOp9lw5YHcu4TImR4yqxGiL6pZwnpciuX/g7qjWBZXn5gf0YBlDsBDDTt5upbP3NguUKgO2qA9M77LyeUwXl3aqbIxYi/VwsQ2t5w9PGWtnOUQQDWUcEg/9dfTb89esZXKATwIDAQAB";
+
+const sharedManifest = {
+ action: {
+ default_icon: {
+ 16: sharedIcons[16],
+ 32: sharedIcons[32]
+ },
+ default_popup: "popup/index.html"
+ },
+ background: {
+ service_worker: "background/index.js"
+ },
+ content_scripts: [
+ {
+ js: ["content/index.js"],
+ matches: [
+ "https://xingtu.cn/ad/creator/market*",
+ "https://*.xingtu.cn/ad/creator/market*"
+ ],
+ run_at: "document_start"
+ }
+ ],
+ description: "Bootstraps the Xingtu creator market content script.",
+ icons: sharedIcons,
+ key: extensionKey,
+ manifest_version: 3,
+ name: "Star Chart Search Enhancer",
+ permissions: ["downloads", "identity", "storage"],
+ version: "0.2.0421.2",
+ web_accessible_resources: [
+ {
+ matches: [
+ "https://xingtu.cn/*",
+ "https://*.xingtu.cn/*"
+ ],
+ resources: ["content/market-page-bridge.js"]
+ }
+ ]
+};
+
+const hostPermissionsByTarget = {
+ development: [
+ "http://*/*",
+ "https://login-api.intelligrow.cn/*",
+ "http://127.0.0.1:4319/*",
+ "https://*/*"
+ ],
+ release: [
+ "https://xingtu.cn/ad/creator/market*",
+ "https://*.xingtu.cn/ad/creator/market*",
+ "https://login-api.intelligrow.cn/*",
+ "https://talent-search.intelligrow.cn/*",
+ "http://192.168.31.21:8083/*"
+ ]
+};
+
+export function createManifest(options = {}) {
+ const target = options.target ?? "development";
+ const hostPermissions = hostPermissionsByTarget[target];
+ if (!hostPermissions) {
+ throw new Error(`Unsupported manifest target: ${target}`);
+ }
+
+ return {
+ ...sharedManifest,
+ host_permissions: hostPermissions
+ };
+}
diff --git a/scripts/package-release.mjs b/scripts/package-release.mjs
new file mode 100644
index 0000000..96ff8fd
--- /dev/null
+++ b/scripts/package-release.mjs
@@ -0,0 +1,24 @@
+import { mkdir, rm } from "node:fs/promises";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const projectRoot = path.resolve(__dirname, "..");
+const sourceDir = path.join(projectRoot, "dist-release");
+const releaseDir = path.join(projectRoot, "release");
+const archivePath = path.join(
+ releaseDir,
+ "star-chart-search-enhancer-internal.zip"
+);
+
+await mkdir(releaseDir, { recursive: true });
+await rm(archivePath, { force: true });
+await execFileAsync("zip", ["-X", "-r", archivePath, "."], {
+ cwd: sourceDir
+});
+
+console.log(`Internal archive created at ${archivePath}`);
diff --git a/src/assets/icons/icon-128.png b/src/assets/icons/icon-128.png
new file mode 100644
index 0000000..8cc212d
Binary files /dev/null and b/src/assets/icons/icon-128.png differ
diff --git a/src/assets/icons/icon-16.png b/src/assets/icons/icon-16.png
new file mode 100644
index 0000000..b2d62c6
Binary files /dev/null and b/src/assets/icons/icon-16.png differ
diff --git a/src/assets/icons/icon-32.png b/src/assets/icons/icon-32.png
new file mode 100644
index 0000000..c87d772
Binary files /dev/null and b/src/assets/icons/icon-32.png differ
diff --git a/src/assets/icons/icon-48.png b/src/assets/icons/icon-48.png
new file mode 100644
index 0000000..ed5f38f
Binary files /dev/null and b/src/assets/icons/icon-48.png differ
diff --git a/src/assets/icons/icon-source.svg b/src/assets/icons/icon-source.svg
new file mode 100644
index 0000000..4c81685
--- /dev/null
+++ b/src/assets/icons/icon-source.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/content/market/batch-payload.ts b/src/content/market/batch-payload.ts
index fbff457..a6268c6 100644
--- a/src/content/market/batch-payload.ts
+++ b/src/content/market/batch-payload.ts
@@ -6,7 +6,6 @@ export interface BatchPayload {
authorId: string;
authorName: string;
}>;
- batchId: string;
batchName: string;
createdAt: string;
creatorName: string;
@@ -40,7 +39,6 @@ export function createBatchPayload(options: {
authorId: record.authorId,
authorName: record.authorName
})),
- batchId: `${logtoUserId}-${options.createdAt}`,
batchName,
createdAt: options.createdAt,
creatorName:
diff --git a/src/content/market/index.ts b/src/content/market/index.ts
index 390f717..ae6d8ce 100644
--- a/src/content/market/index.ts
+++ b/src/content/market/index.ts
@@ -79,15 +79,12 @@ export function createMarketController(options: CreateMarketControllerOptions) {
options.submitBatch ??
((payload: BatchPayload) =>
readBatchSubmitAck(sendRuntimeMessage, payload));
+ let activeProgressLabel = "导出中";
+ let shouldShowDetailedProgress = true;
const exportRangeController = createExportRangeController({
document: options.document,
onProgress: ({ currentPage, totalPages }) => {
- setToolbarExportStatus(
- toolbar,
- totalPages
- ? `导出中 ${currentPage}/${totalPages} 页...`
- : `导出中 第${currentPage}页...`
- );
+ updateToolbarProgress(currentPage, totalPages);
},
prepareCurrentPageForExport: prepareCurrentPageForExport,
readCurrentPageRecords: () => getVisibleOrderedRecords(),
@@ -97,12 +94,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
const silentExportController = createSilentExportController({
document: options.document,
onProgress: ({ currentPage, totalPages }) => {
- setToolbarExportStatus(
- toolbar,
- totalPages
- ? `导出中 ${currentPage}/${totalPages} 页...`
- : `导出中 第${currentPage}页...`
- );
+ updateToolbarProgress(currentPage, totalPages);
}
});
let activeSort: MarketSortState | undefined;
@@ -155,7 +147,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
setToolbarBusyState(toolbar, true);
try {
const records = filterRecordsBySelection(
- await exportRecords(exportTarget.target)
+ await exportRecords(exportTarget.target, "导出中", {
+ showDetailedProgress: selectedAuthorIds.size === 0
+ })
);
options.onCsvReady?.(buildCsv(records));
setToolbarExportStatus(toolbar, "");
@@ -477,8 +471,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
async function exportRecords(
target: MarketExportTarget,
- inProgressLabel = "导出中"
+ inProgressLabel = "导出中",
+ progressOptions: {
+ showDetailedProgress?: boolean;
+ } = {}
): Promise {
+ activeProgressLabel = inProgressLabel;
+ shouldShowDetailedProgress = progressOptions.showDetailedProgress ?? true;
setToolbarExportStatus(toolbar, `${inProgressLabel}...`);
if (target.mode === "count" && target.pageCount <= 1) {
@@ -499,6 +498,23 @@ export function createMarketController(options: CreateMarketControllerOptions) {
return exportRangeController.exportRecords(target);
}
+ function updateToolbarProgress(
+ currentPage: number,
+ totalPages: number | undefined
+ ): void {
+ if (!shouldShowDetailedProgress) {
+ setToolbarExportStatus(toolbar, `${activeProgressLabel}...`);
+ return;
+ }
+
+ setToolbarExportStatus(
+ toolbar,
+ totalPages
+ ? `${activeProgressLabel} ${currentPage}/${totalPages} 页...`
+ : `${activeProgressLabel} 第${currentPage}页...`
+ );
+ }
+
function filterRecordsBySelection(records: MarketRecord[]): MarketRecord[] {
if (selectedAuthorIds.size === 0) {
return records;
diff --git a/src/manifest.json b/src/manifest.json
index bb53cd5..d0aafba 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -3,7 +3,14 @@
"name": "Star Chart Search Enhancer",
"version": "0.2.0421.2",
"description": "Bootstraps the Xingtu creator market content script.",
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CaZJxcX97TbRXCR08L10t9EZFV31+wPnUgDf21j2f0qYaWdblzWXfVkeU9jGb2Hr2Etpp7F/XuBa6pcipUXkzMMBkJ42KOkciAwbuzTBoAtGB8o9aoWigtax+gGfSz+T3BjqxKBJtXqeqbIAKCDIlxRKIrY+KcY1Z+mD5BKcBHKsUDQPlHsrjc1g0wIBD5doz9LoOk1Wso6gK5cSeOp9lw5YHcu4TImR4yqxGiL6pZwnpciuX/g7qjWBZXn5gf0YBlDsBDDTt5upbP3NguUKgO2qA9M77LyeUwXl3aqbIxYi/VwsQ2t5w9PGWtnOUQQDWUcEg/9dfTb89esZXKATwIDAQAB",
"permissions": ["downloads", "identity", "storage"],
+ "icons": {
+ "16": "assets/icons/icon-16.png",
+ "32": "assets/icons/icon-32.png",
+ "48": "assets/icons/icon-48.png",
+ "128": "assets/icons/icon-128.png"
+ },
"host_permissions": [
"http://*/*",
"https://login-api.intelligrow.cn/*",
@@ -11,6 +18,10 @@
"https://*/*"
],
"action": {
+ "default_icon": {
+ "16": "assets/icons/icon-16.png",
+ "32": "assets/icons/icon-32.png"
+ },
"default_popup": "popup/index.html"
},
"background": {
diff --git a/src/shared/auth-config.ts b/src/shared/auth-config.ts
index 99fcd5c..3390c2c 100644
--- a/src/shared/auth-config.ts
+++ b/src/shared/auth-config.ts
@@ -9,7 +9,7 @@ export interface AuthConfig {
const defaultAuthConfig: AuthConfig = {
apiResource: "https://talent-search.intelligrow.cn",
appId: "i4jkllbvih0554r4n0fd3",
- enableDevAuthPanel: true,
+ enableDevAuthPanel: false,
logtoEndpoint: "https://login-api.intelligrow.cn",
scopes: ["openid", "profile", "offline_access", "talent-search:read"]
};
diff --git a/tests/auth-config.test.ts b/tests/auth-config.test.ts
index 2fca477..4219109 100644
--- a/tests/auth-config.test.ts
+++ b/tests/auth-config.test.ts
@@ -7,7 +7,7 @@ describe("auth-config", () => {
expect(readAuthConfig()).toEqual({
apiResource: "https://talent-search.intelligrow.cn",
appId: "i4jkllbvih0554r4n0fd3",
- enableDevAuthPanel: true,
+ enableDevAuthPanel: false,
logtoEndpoint: "https://login-api.intelligrow.cn",
scopes: [
"openid",
diff --git a/tests/background-index.test.ts b/tests/background-index.test.ts
index 5d166f9..64ab09f 100644
--- a/tests/background-index.test.ts
+++ b/tests/background-index.test.ts
@@ -164,7 +164,6 @@ describe("background-index", () => {
{
payload: {
authors: [{ authorId: "111", authorName: "达人A" }],
- batchId: "批次A-2026-04-22T12:30:00.000Z",
batchName: "批次A",
createdAt: "2026-04-22T12:30:00.000Z",
creatorName: "王少卿",
@@ -182,9 +181,10 @@ describe("background-index", () => {
expect(submitBatch).toHaveBeenCalledWith(
expect.objectContaining({
- batchId: "批次A-2026-04-22T12:30:00.000Z"
+ batchName: "批次A"
})
);
+ expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId");
expect(sendResponse).toHaveBeenCalledWith({
ok: true,
type: "batch:ack",
diff --git a/tests/batch-payload.test.ts b/tests/batch-payload.test.ts
index b93d8a0..22ab26b 100644
--- a/tests/batch-payload.test.ts
+++ b/tests/batch-payload.test.ts
@@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest";
import { createBatchPayload } from "../src/content/market/batch-payload";
describe("batch-payload", () => {
- test("builds a batch id from the user id and timestamp", () => {
+ test("builds the batch payload without a client-side batch id", () => {
const payload = createBatchPayload({
authState: {
isAuthenticated: true,
@@ -26,7 +26,6 @@ describe("batch-payload", () => {
{ authorId: "111", authorName: "达人A" },
{ authorId: "222", authorName: "达人B" }
],
- batchId: "p7pdhhtde8kj-2026-04-22T12:30:00.000Z",
batchName: "618达人筛选第一批",
createdAt: "2026-04-22T12:30:00.000Z",
creatorName: "王少卿",
diff --git a/tests/batch-submit-client.test.ts b/tests/batch-submit-client.test.ts
index ddc8c7e..fbef987 100644
--- a/tests/batch-submit-client.test.ts
+++ b/tests/batch-submit-client.test.ts
@@ -36,7 +36,6 @@ describe("batch-submit-client", () => {
await client.submitBatch({
authors: [{ authorId: "111", authorName: "达人A" }],
- batchId: "批次A-2026-04-22T12:30:00.000Z",
batchName: "批次A",
createdAt: "2026-04-22T12:30:00.000Z",
creatorName: "王少卿",
@@ -49,7 +48,6 @@ describe("batch-submit-client", () => {
expect.objectContaining({
body: JSON.stringify({
authors: [{ authorId: "111", authorName: "达人A" }],
- batchId: "批次A-2026-04-22T12:30:00.000Z",
batchName: "批次A",
createdAt: "2026-04-22T12:30:00.000Z",
creatorName: "王少卿",
@@ -87,7 +85,6 @@ describe("batch-submit-client", () => {
await expect(
client.submitBatch({
authors: [],
- batchId: "批次A-2026-04-22T12:30:00.000Z",
batchName: "批次A",
createdAt: "2026-04-22T12:30:00.000Z",
creatorName: "王少卿",
@@ -115,7 +112,6 @@ describe("batch-submit-client", () => {
await expect(
client.submitBatch({
authors: [],
- batchId: "批次A-2026-04-22T12:30:00.000Z",
batchName: "批次A",
createdAt: "2026-04-22T12:30:00.000Z",
creatorName: "王少卿",
diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts
index 97c0bac..80f8694 100644
--- a/tests/manifest.test.ts
+++ b/tests/manifest.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, test } from "vitest";
import manifest from "../src/manifest.json";
+import { createManifest } from "../scripts/manifest.mjs";
describe("manifest", () => {
test("injects the content script on the www Xingtu market page", () => {
@@ -16,6 +17,9 @@ describe("manifest", () => {
expect(manifest.permissions).toEqual(
expect.arrayContaining(["downloads", "identity", "storage"])
);
+ expect(manifest.key).toBe(
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CaZJxcX97TbRXCR08L10t9EZFV31+wPnUgDf21j2f0qYaWdblzWXfVkeU9jGb2Hr2Etpp7F/XuBa6pcipUXkzMMBkJ42KOkciAwbuzTBoAtGB8o9aoWigtax+gGfSz+T3BjqxKBJtXqeqbIAKCDIlxRKIrY+KcY1Z+mD5BKcBHKsUDQPlHsrjc1g0wIBD5doz9LoOk1Wso6gK5cSeOp9lw5YHcu4TImR4yqxGiL6pZwnpciuX/g7qjWBZXn5gf0YBlDsBDDTt5upbP3NguUKgO2qA9M77LyeUwXl3aqbIxYi/VwsQ2t5w9PGWtnOUQQDWUcEg/9dfTb89esZXKATwIDAQAB"
+ );
expect(manifest.host_permissions).toEqual(
expect.arrayContaining([
"http://*/*",
@@ -27,4 +31,38 @@ describe("manifest", () => {
expect(manifest.background?.service_worker).toBe("background/index.js");
expect(manifest.action?.default_popup).toBe("popup/index.html");
});
+
+ test("builds a release manifest with narrowed host permissions", () => {
+ const releaseManifest = createManifest({ target: "release" });
+
+ expect(releaseManifest.permissions).toEqual(
+ expect.arrayContaining(["downloads", "identity", "storage"])
+ );
+ expect(releaseManifest.host_permissions).toEqual([
+ "https://xingtu.cn/ad/creator/market*",
+ "https://*.xingtu.cn/ad/creator/market*",
+ "https://login-api.intelligrow.cn/*",
+ "https://talent-search.intelligrow.cn/*",
+ "http://192.168.31.21:8083/*"
+ ]);
+ expect(releaseManifest.host_permissions).not.toEqual(
+ expect.arrayContaining(["http://*/*", "https://*/*", "http://127.0.0.1:4319/*"])
+ );
+ });
+
+ test("builds a release manifest with extension icons", () => {
+ const releaseManifest = createManifest({ target: "release" });
+
+ expect(releaseManifest.icons).toEqual({
+ "16": "assets/icons/icon-16.png",
+ "32": "assets/icons/icon-32.png",
+ "48": "assets/icons/icon-48.png",
+ "128": "assets/icons/icon-128.png"
+ });
+ expect(releaseManifest.key).toBe(manifest.key);
+ expect(releaseManifest.action?.default_icon).toEqual({
+ "16": "assets/icons/icon-16.png",
+ "32": "assets/icons/icon-32.png"
+ });
+ });
});
diff --git a/tests/market-content-entry.test.ts b/tests/market-content-entry.test.ts
index 126bc81..b4278c2 100644
--- a/tests/market-content-entry.test.ts
+++ b/tests/market-content-entry.test.ts
@@ -1535,6 +1535,117 @@ describe("market-content-entry", () => {
]);
});
+ test(
+ "selected export keeps a generic loading status while exporting the default paged range",
+ async () => {
+ const pages = [
+ [
+ { authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
+ { authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
+ ],
+ [{ authorId: "333", authorName: "达人 C", price21To60s: "¥33,000" }],
+ [{ authorId: "444", authorName: "达人 D", price21To60s: "¥44,000" }],
+ [{ authorId: "555", authorName: "达人 E", price21To60s: "¥55,000" }],
+ [{ authorId: "666", authorName: "达人 F", price21To60s: "¥66,000" }]
+ ];
+ const secondPageDeferred = createDeferred<{
+ json(): Promise;
+ ok: boolean;
+ }>();
+
+ document.body.innerHTML = buildRealMarketFixture(pages[0]);
+ const buildCsv = vi.fn(() => "csv-output");
+ const fetchMock = vi.fn(async (_input: string, init?: RequestInit) => {
+ const body = JSON.parse(String(init?.body ?? "{}")) as { page?: number };
+ const pageNumber = body.page ?? 1;
+ const response = {
+ json: async () => ({
+ data: {
+ marketList: buildMarketListResponseRows(pages[pageNumber - 1] ?? []),
+ totalPages: 5
+ }
+ }),
+ ok: true
+ };
+
+ if (pageNumber === 2) {
+ return secondPageDeferred.promise;
+ }
+
+ return response;
+ });
+
+ (
+ globalThis as typeof globalThis & {
+ fetch?: typeof fetchMock;
+ }
+ ).fetch = fetchMock;
+ document.documentElement.setAttribute(
+ "data-sces-market-request-snapshot",
+ JSON.stringify({
+ body: JSON.stringify({
+ page: 1
+ }),
+ method: "POST",
+ url: "https://xingtu.cn/api/mock-market-search"
+ })
+ );
+
+ const { createMarketController } = await import("../src/content/market/index");
+ const controller = trackController(createMarketController({
+ buildCsv,
+ document,
+ loadAuthorMetrics: async () => ({
+ success: false,
+ reason: "request-failed"
+ }),
+ onCsvReady: vi.fn(),
+ window
+ }));
+
+ await controller.ready;
+ clickSelectionCheckboxForAuthor("111");
+
+ click('[data-plugin-export="button"]');
+ for (let attempt = 0; attempt < 40; attempt += 1) {
+ if (
+ fetchMock.mock.calls.some(([, init]) => {
+ const body = JSON.parse(
+ String((init as RequestInit | undefined)?.body ?? "{}")
+ ) as { page?: number };
+ return body.page === 2;
+ })
+ ) {
+ break;
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ await Promise.resolve();
+ }
+
+ expect(
+ document.querySelector('[data-plugin-export-status="text"]')?.textContent
+ ).toBe("导出中...");
+
+ secondPageDeferred.resolve({
+ json: async () => ({
+ data: {
+ marketList: buildMarketListResponseRows(pages[1]),
+ totalPages: 5
+ }
+ }),
+ ok: true
+ });
+
+ await waitForMockCall(buildCsv, 120, 50);
+
+ expect(buildCsv.mock.calls[0][0].map((record) => record.authorId)).toEqual([
+ "111"
+ ]);
+ },
+ 15000
+ );
+
test("selected export falls back to all creators in the current range when no selection matches", async () => {
const pages = [
[
@@ -1610,11 +1721,11 @@ describe("market-content-entry", () => {
expect(promptBatchName).toHaveBeenCalledTimes(1);
expect(submitBatch).toHaveBeenCalledWith(
expect.objectContaining({
- batchId: expect.stringContaining("p7pdhhtde8kj-"),
batchName: "618达人筛选第一批",
logtoUserId: "p7pdhhtde8kj"
})
);
+ expect(submitBatch.mock.calls[0]?.[0]).not.toHaveProperty("batchId");
});
test("selected batch submit uses only creators selected in the current range", async () => {
diff --git a/tests/mock-protected-api.test.ts b/tests/mock-protected-api.test.ts
index 8081dfa..0037081 100644
--- a/tests/mock-protected-api.test.ts
+++ b/tests/mock-protected-api.test.ts
@@ -53,7 +53,6 @@ describe("mock-protected-api", () => {
const response = await fetch(`${server.baseUrl}/api/mock/batches`, {
body: JSON.stringify({
authors: [{ authorId: "111", authorName: "达人A" }],
- batchId: "批次A-2026-04-22T12:30:00.000Z",
batchName: "批次A",
createdAt: "2026-04-22T12:30:00.000Z",
creatorName: "王少卿",
@@ -71,7 +70,7 @@ describe("mock-protected-api", () => {
await expect(response.json()).resolves.toEqual(
expect.objectContaining({
acceptedCount: 1,
- batchId: "批次A-2026-04-22T12:30:00.000Z",
+ batchId: null,
ok: true,
source: "mock-batch-submit"
})