feat: honor all-range export and batch scope

This commit is contained in:
wxs
2026-07-27 16:00:57 +08:00
parent e15bc0657a
commit e03dedd91e
5 changed files with 293 additions and 47 deletions
+162 -2
View File
@@ -1791,7 +1791,7 @@ describe("market-content-entry", () => {
});
});
test("audience profile export requires selected creators", async () => {
test("audience profile export requires selected creators outside of the all range", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
]);
@@ -1825,6 +1825,109 @@ describe("market-content-entry", () => {
).toContain("请先勾选需要导出数据的达人");
});
test("audience profile export includes all filtered creators when all is selected without checks", async () => {
const records = [
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
];
document.body.innerHTML = buildRealMarketFixture(records);
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"
})
);
(
globalThis as typeof globalThis & { fetch?: typeof fetch }
).fetch = vi.fn(async () => ({
json: async () => ({
data: {
marketList: buildMarketListResponseRows(records),
totalPages: 1
}
}),
ok: true
}));
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
buildAudienceProfileCsv,
document,
loadAudienceProfile: async () => ({ status: "success" as const }),
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
onCsvReady: vi.fn(),
window
}));
await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]');
click('[data-plugin-export-audience-profile="button"]');
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
expect(
buildAudienceProfileCsv.mock.calls[0]?.[0].map(({ record }) => record.authorId)
).toEqual(["111", "222"]);
});
test("audience profile export gives checked creators priority in the all range", async () => {
const records = [
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
];
document.body.innerHTML = buildRealMarketFixture(records);
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"
})
);
(
globalThis as typeof globalThis & { fetch?: typeof fetch }
).fetch = vi.fn(async () => ({
json: async () => ({
data: {
marketList: buildMarketListResponseRows(records),
totalPages: 1
}
}),
ok: true
}));
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
buildAudienceProfileCsv,
document,
loadAudienceProfile: async () => ({ status: "success" as const }),
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
onCsvReady: vi.fn(),
window
}));
await controller.ready;
clickSelectionCheckboxForAuthor("222");
setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]');
click('[data-plugin-export-audience-profile="button"]');
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
expect(
buildAudienceProfileCsv.mock.calls[0]?.[0].map(({ record }) => record.authorId)
).toEqual(["222"]);
});
test("audience profile export loads profiles only for selected creators", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" },
@@ -2967,8 +3070,62 @@ describe("market-content-entry", () => {
);
});
test("all-range batch submit does not fall back when checked creators are outside the results", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
]);
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"
})
);
(
globalThis as typeof globalThis & { fetch?: typeof fetch }
).fetch = vi.fn(async () => ({
json: async () => ({
data: {
marketList: buildMarketListResponseRows([
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
]),
totalPages: 1
}
}),
ok: true
}));
const submitBatch = vi.fn(async () => ({ ok: true }));
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
document,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
promptBatchName: vi.fn(() => "自动选择批次"),
submitBatch,
window
}));
await controller.ready;
clickSelectionCheckboxForAuthor("111");
setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
await waitForCondition(() =>
document.querySelector('[data-plugin-export-status="text"]')?.textContent ===
"全部范围内没有选中的达人"
);
expect(submitBatch).not.toHaveBeenCalled();
});
test(
"default paged batch submit keeps detailed progress when no creators are selected",
"all-range batch submit keeps detailed progress when no creators are selected",
async () => {
const pages = [
[
@@ -3042,6 +3199,9 @@ describe("market-content-entry", () => {
await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "all");
dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-batch-submit="button"]');
for (let attempt = 0; attempt < 40; attempt += 1) {
+50
View File
@@ -155,6 +155,56 @@ describe("silent-export-controller", () => {
expect(records?.map((record) => record.authorId)).toEqual(["2", "3"]);
});
test("starts all-result exports from page 1 even after browsing to a later page", async () => {
document.documentElement.setAttribute(
"data-sces-market-request-snapshot",
JSON.stringify({
body: JSON.stringify({
page_param: {
page: 2
}
}),
method: "POST",
url: "https://xingtu.cn/api/mock-market-search"
})
);
const requestedPages: number[] = [];
const controller = createSilentExportController({
document,
fetchImpl: async (_url, init) => {
const body = JSON.parse(String(init?.body ?? "{}")) as {
page_param?: { page?: number };
};
const pageNo = body.page_param?.page ?? 0;
requestedPages.push(pageNo);
return {
json: async () => ({
authors: [
{
attribute_datas: {
nickname: `达人${pageNo}`
},
star_id: String(pageNo)
}
],
pagination: {
page: pageNo,
totalPages: 3
}
}),
ok: true
};
}
});
const records = await controller.exportRecords({ mode: "all" });
expect(requestedPages).toEqual([1, 2, 3]);
expect(records?.map((record) => record.authorId)).toEqual(["1", "2", "3"]);
});
test("keeps attribute_datas.id as the spread author id while preserving star_id as row id", async () => {
document.documentElement.setAttribute(
"data-sces-market-request-snapshot",