feat: add logto auth and backend metrics integration
This commit is contained in:
@@ -0,0 +1,697 @@
|
||||
# Market Batch Submit Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a standalone `提交批次` toolbar action that collects the currently selected export range of Xingtu creators, prompts for a batch name, builds a batch payload with Logto user identity plus creator IDs, and submits the batch to a protected API endpoint with a Bearer token.
|
||||
|
||||
**Architecture:** Reuse the existing export-range collection path so multi-page batch submission behaves exactly like CSV export. Keep responsibilities split: toolbar UI emits a submit intent, a batch-payload module assembles the request body, and a protected batch-submit client owns token injection plus POST behavior. For first-pass verification, extend the local mock server with a batch endpoint that echoes the submitted payload.
|
||||
|
||||
**Tech Stack:** Chrome MV3, TypeScript, Vitest, tsup, Node HTTP server, `@logto/chrome-extension`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### New files
|
||||
|
||||
- `src/content/market/batch-payload.ts`
|
||||
- `src/shared/batch-submit-client.ts`
|
||||
- `tests/batch-payload.test.ts`
|
||||
- `tests/batch-submit-client.test.ts`
|
||||
|
||||
### Existing files to modify
|
||||
|
||||
- `src/content/market/plugin-toolbar.ts`
|
||||
- `src/content/market/index.ts`
|
||||
- `src/shared/auth-messages.ts`
|
||||
- `src/background/auth/state.ts`
|
||||
- `src/popup/view.ts`
|
||||
- `scripts/mock-protected-api.mjs`
|
||||
- `tests/market-content-entry.test.ts`
|
||||
- `tests/popup-entry.test.ts`
|
||||
- `tests/mock-protected-api.test.ts`
|
||||
- `README.md`
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- `src/content/market/plugin-toolbar.ts`: add a dedicated submit button and include it in busy-state handling.
|
||||
- `src/content/market/index.ts`: orchestrate prompt input, range collection reuse, payload creation, and batch submission.
|
||||
- `src/content/market/batch-payload.ts`: create the batch payload from batch name, timestamp, auth state, and market records.
|
||||
- `src/shared/batch-submit-client.ts`: POST JSON to the protected batch endpoint with `Authorization: Bearer <token>`.
|
||||
- `src/shared/auth-messages.ts` and `src/background/auth/state.ts`: expose stable access to Logto user `sub` and display name already present in auth state.
|
||||
- `scripts/mock-protected-api.mjs`: add a local `/api/mock/batches` endpoint that validates Bearer auth and echoes the payload.
|
||||
|
||||
## Task 1: Add The Batch Payload Builder
|
||||
|
||||
**Files:**
|
||||
- Create: `src/content/market/batch-payload.ts`
|
||||
- Create: `tests/batch-payload.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing batch payload tests**
|
||||
|
||||
Create `tests/batch-payload.test.ts` with cases for:
|
||||
|
||||
```ts
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createBatchPayload } from "../src/content/market/batch-payload";
|
||||
|
||||
describe("batch-payload", () => {
|
||||
test("builds a batch id from the batch name and timestamp", () => {
|
||||
const payload = createBatchPayload({
|
||||
authState: {
|
||||
isAuthenticated: true,
|
||||
resource: "https://talent-search.intelligrow.cn",
|
||||
userInfo: {
|
||||
name: "王少卿",
|
||||
sub: "p7pdhhtde8kj"
|
||||
}
|
||||
},
|
||||
batchName: "618达人筛选第一批",
|
||||
createdAt: "2026-04-22T12:30:00.000Z",
|
||||
records: [
|
||||
{ authorId: "111", authorName: "达人A", status: "success" },
|
||||
{ authorId: "222", authorName: "达人B", status: "success" }
|
||||
]
|
||||
});
|
||||
|
||||
expect(payload).toEqual({
|
||||
authors: [
|
||||
{ authorId: "111", authorName: "达人A" },
|
||||
{ authorId: "222", authorName: "达人B" }
|
||||
],
|
||||
batchId: "618达人筛选第一批-2026-04-22T12:30:00.000Z",
|
||||
batchName: "618达人筛选第一批",
|
||||
createdAt: "2026-04-22T12:30:00.000Z",
|
||||
creatorName: "王少卿",
|
||||
logtoUserId: "p7pdhhtde8kj",
|
||||
resource: "https://talent-search.intelligrow.cn"
|
||||
});
|
||||
});
|
||||
|
||||
test("throws when the user id is unavailable", () => {
|
||||
expect(() =>
|
||||
createBatchPayload({
|
||||
authState: {
|
||||
isAuthenticated: true,
|
||||
resource: "https://talent-search.intelligrow.cn",
|
||||
userInfo: {
|
||||
name: "王少卿"
|
||||
}
|
||||
},
|
||||
batchName: "批次A",
|
||||
createdAt: "2026-04-22T12:30:00.000Z",
|
||||
records: [{ authorId: "111", authorName: "达人A", status: "success" }]
|
||||
})
|
||||
).toThrow(/user/i);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the batch payload test to verify red**
|
||||
|
||||
Run: `npm test -- tests/batch-payload.test.ts`
|
||||
|
||||
Expected: FAIL because `createBatchPayload` does not exist yet
|
||||
|
||||
- [ ] **Step 3: Implement the minimal batch payload builder**
|
||||
|
||||
Create `src/content/market/batch-payload.ts` with:
|
||||
|
||||
```ts
|
||||
import type { AuthStateValue } from "../../shared/auth-messages";
|
||||
import type { MarketRecord } from "./types";
|
||||
|
||||
export interface BatchPayload {
|
||||
authors: Array<{
|
||||
authorId: string;
|
||||
authorName: string;
|
||||
}>;
|
||||
batchId: string;
|
||||
batchName: string;
|
||||
createdAt: string;
|
||||
creatorName: string;
|
||||
logtoUserId: string;
|
||||
resource: string;
|
||||
}
|
||||
|
||||
export function createBatchPayload(options: {
|
||||
authState: AuthStateValue;
|
||||
batchName: string;
|
||||
createdAt: string;
|
||||
records: MarketRecord[];
|
||||
}): BatchPayload {
|
||||
const logtoUserId = options.authState.userInfo?.sub?.trim();
|
||||
if (!logtoUserId) {
|
||||
throw new Error("batch submit user id unavailable");
|
||||
}
|
||||
|
||||
const resource = options.authState.resource?.trim();
|
||||
if (!resource) {
|
||||
throw new Error("batch submit resource unavailable");
|
||||
}
|
||||
|
||||
const batchName = options.batchName.trim();
|
||||
if (!batchName) {
|
||||
throw new Error("batch submit batch name is required");
|
||||
}
|
||||
|
||||
return {
|
||||
authors: options.records.map((record) => ({
|
||||
authorId: record.authorId,
|
||||
authorName: record.authorName
|
||||
})),
|
||||
batchId: `${batchName}-${options.createdAt}`,
|
||||
batchName,
|
||||
createdAt: options.createdAt,
|
||||
creatorName:
|
||||
options.authState.userInfo?.name ??
|
||||
options.authState.userInfo?.username ??
|
||||
logtoUserId,
|
||||
logtoUserId,
|
||||
resource
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the batch payload test to verify green**
|
||||
|
||||
Run: `npm test -- tests/batch-payload.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit the payload builder slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add src/content/market/batch-payload.ts tests/batch-payload.test.ts
|
||||
git commit -m "feat: add batch payload builder"
|
||||
```
|
||||
|
||||
## Task 2: Build The Protected Batch Submit Client
|
||||
|
||||
**Files:**
|
||||
- Create: `src/shared/batch-submit-client.ts`
|
||||
- Create: `tests/batch-submit-client.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing batch submit client tests**
|
||||
|
||||
Create `tests/batch-submit-client.test.ts` with:
|
||||
|
||||
```ts
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { createBatchSubmitClient } from "../src/shared/batch-submit-client";
|
||||
|
||||
describe("batch-submit-client", () => {
|
||||
test("posts the batch payload with a Bearer token", async () => {
|
||||
const sendMessage = vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:token",
|
||||
value: { accessToken: "abc123" }
|
||||
}));
|
||||
const fetchImpl = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ ok: true, acceptedCount: 2 })
|
||||
}));
|
||||
|
||||
const client = createBatchSubmitClient({
|
||||
baseUrl: "http://127.0.0.1:4319",
|
||||
fetchImpl,
|
||||
sendMessage
|
||||
});
|
||||
|
||||
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: "王少卿",
|
||||
logtoUserId: "p7pdhhtde8kj",
|
||||
resource: "https://talent-search.intelligrow.cn"
|
||||
});
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
"http://127.0.0.1:4319/api/mock/batches",
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify(
|
||||
expect.objectContaining({
|
||||
batchId: "批次A-2026-04-22T12:30:00.000Z"
|
||||
})
|
||||
),
|
||||
headers: expect.objectContaining({
|
||||
Authorization: "Bearer abc123",
|
||||
"Content-Type": "application/json"
|
||||
}),
|
||||
method: "POST"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("throws on unauthorized responses", async () => {
|
||||
const client = createBatchSubmitClient({
|
||||
baseUrl: "http://127.0.0.1:4319",
|
||||
fetchImpl: vi.fn(async () => ({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: async () => ({ ok: false, error: "unauthorized" })
|
||||
})),
|
||||
sendMessage: vi.fn(async () => ({
|
||||
ok: true,
|
||||
type: "auth:token",
|
||||
value: { accessToken: "abc123" }
|
||||
}))
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.submitBatch({
|
||||
authors: [],
|
||||
batchId: "批次A-2026-04-22T12:30:00.000Z",
|
||||
batchName: "批次A",
|
||||
createdAt: "2026-04-22T12:30:00.000Z",
|
||||
creatorName: "王少卿",
|
||||
logtoUserId: "p7pdhhtde8kj",
|
||||
resource: "https://talent-search.intelligrow.cn"
|
||||
})
|
||||
).rejects.toThrow(/unauthorized/i);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the batch submit client test to verify red**
|
||||
|
||||
Run: `npm test -- tests/batch-submit-client.test.ts`
|
||||
|
||||
Expected: FAIL because `createBatchSubmitClient` does not exist yet
|
||||
|
||||
- [ ] **Step 3: Implement the minimal batch submit client**
|
||||
|
||||
Create `src/shared/batch-submit-client.ts` with:
|
||||
|
||||
```ts
|
||||
import { isAuthResponseMessage } from "./auth-messages";
|
||||
import type { BatchPayload } from "../content/market/batch-payload";
|
||||
|
||||
interface FetchResponseLike {
|
||||
json(): Promise<unknown>;
|
||||
ok: boolean;
|
||||
status: number;
|
||||
}
|
||||
|
||||
type FetchLike = (
|
||||
input: string,
|
||||
init?: RequestInit
|
||||
) => Promise<FetchResponseLike>;
|
||||
|
||||
type SendMessageLike = (message: unknown) => Promise<unknown>;
|
||||
|
||||
export function createBatchSubmitClient(options: {
|
||||
baseUrl: string;
|
||||
fetchImpl?: FetchLike;
|
||||
sendMessage: SendMessageLike;
|
||||
}) {
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
|
||||
return {
|
||||
async submitBatch(payload: BatchPayload) {
|
||||
const token = await readAccessToken(options.sendMessage);
|
||||
const response = await fetchImpl(
|
||||
new URL("/api/mock/batches", options.baseUrl).toString(),
|
||||
{
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST"
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error("batch submit unauthorized");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`batch submit failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function readAccessToken(sendMessage: SendMessageLike): Promise<string> {
|
||||
const response = await sendMessage({ type: "auth:get-access-token" });
|
||||
if (
|
||||
!isAuthResponseMessage(response) ||
|
||||
!response.ok ||
|
||||
response.type !== "auth:token" ||
|
||||
!response.value.accessToken.trim()
|
||||
) {
|
||||
throw new Error("batch submit token unavailable");
|
||||
}
|
||||
return response.value.accessToken;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the batch submit client test to verify green**
|
||||
|
||||
Run: `npm test -- tests/batch-submit-client.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit the batch submit client slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add src/shared/batch-submit-client.ts tests/batch-submit-client.test.ts
|
||||
git commit -m "feat: add batch submit client"
|
||||
```
|
||||
|
||||
## Task 3: Extend The Toolbar With A Batch Submit Action
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/content/market/plugin-toolbar.ts`
|
||||
- Modify: `tests/market-content-entry.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing toolbar tests**
|
||||
|
||||
Add tests to `tests/market-content-entry.test.ts` for:
|
||||
|
||||
```ts
|
||||
test("renders a batch submit button in the toolbar", async () => {
|
||||
document.body.innerHTML = buildMarketFixture();
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = createMarketController({
|
||||
document,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: true,
|
||||
rates: {
|
||||
personalVideoAfterSearchRate: "0.01% - 0.1%",
|
||||
singleVideoAfterSearchRate: "0.01% - 0.1%"
|
||||
}
|
||||
}),
|
||||
window
|
||||
});
|
||||
|
||||
await controller.ready;
|
||||
|
||||
expect(document.querySelector('[data-plugin-batch-submit="button"]')).not.toBeNull();
|
||||
});
|
||||
```
|
||||
|
||||
Also add a test that `setToolbarBusyState()` disables the batch submit button.
|
||||
|
||||
- [ ] **Step 2: Run the focused market toolbar tests to verify red**
|
||||
|
||||
Run: `npm test -- tests/market-content-entry.test.ts`
|
||||
|
||||
Expected: FAIL because the batch submit button does not exist yet
|
||||
|
||||
- [ ] **Step 3: Add the minimal toolbar button**
|
||||
|
||||
Update `src/content/market/plugin-toolbar.ts`:
|
||||
|
||||
- extend `PluginToolbarHandlers` with `onSubmitBatch()`
|
||||
- extend `PluginToolbarDom` with `batchSubmitButton: HTMLButtonElement`
|
||||
- create a new button:
|
||||
|
||||
```ts
|
||||
const batchSubmitButton = document.createElement("button");
|
||||
batchSubmitButton.type = "button";
|
||||
batchSubmitButton.dataset.pluginBatchSubmit = "button";
|
||||
batchSubmitButton.textContent = "提交批次";
|
||||
```
|
||||
|
||||
- append it next to `exportButton`
|
||||
- wire its click handler to `handlers.onSubmitBatch()`
|
||||
- include it in `readToolbarDom()` and `setToolbarBusyState()`
|
||||
|
||||
- [ ] **Step 4: Re-run the focused market toolbar tests to verify green**
|
||||
|
||||
Run: `npm test -- tests/market-content-entry.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit the toolbar slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add src/content/market/plugin-toolbar.ts tests/market-content-entry.test.ts
|
||||
git commit -m "feat: add batch submit toolbar action"
|
||||
```
|
||||
|
||||
## Task 4: Wire Batch Submission Into The Market Controller
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/content/market/index.ts`
|
||||
- Modify: `src/shared/auth-messages.ts`
|
||||
- Modify: `src/background/auth/state.ts`
|
||||
- Modify: `tests/market-content-entry.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing controller tests**
|
||||
|
||||
Add tests to `tests/market-content-entry.test.ts` for:
|
||||
|
||||
```ts
|
||||
test("prompts for a batch name before submitting the current range", async () => {
|
||||
document.body.innerHTML = buildMarketFixture();
|
||||
const prompt = vi.fn(() => "618达人筛选第一批");
|
||||
const submitBatch = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
const { createMarketController } = await import("../src/content/market/index");
|
||||
const controller = createMarketController({
|
||||
document,
|
||||
getAuthState: async () => ({
|
||||
isAuthenticated: true,
|
||||
resource: "https://talent-search.intelligrow.cn",
|
||||
userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" }
|
||||
}),
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: true,
|
||||
rates: {
|
||||
personalVideoAfterSearchRate: "0.01% - 0.1%",
|
||||
singleVideoAfterSearchRate: "0.01% - 0.1%"
|
||||
}
|
||||
}),
|
||||
promptBatchName: prompt,
|
||||
submitBatch,
|
||||
window
|
||||
});
|
||||
|
||||
await controller.ready;
|
||||
(document.querySelector('[data-plugin-batch-submit="button"]') as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(prompt).toHaveBeenCalled();
|
||||
expect(submitBatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
batchName: "618达人筛选第一批",
|
||||
logtoUserId: "p7pdhhtde8kj"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("shows an error when the batch name is blank", async () => {
|
||||
// same setup, but prompt returns " "
|
||||
// expect toolbar status to contain "请输入批次名称"
|
||||
});
|
||||
|
||||
test("does nothing when the prompt is cancelled", async () => {
|
||||
// prompt returns null
|
||||
// submitBatch is not called
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused market controller tests to verify red**
|
||||
|
||||
Run: `npm test -- tests/market-content-entry.test.ts`
|
||||
|
||||
Expected: FAIL because batch submission wiring does not exist yet
|
||||
|
||||
- [ ] **Step 3: Add the minimal market controller wiring**
|
||||
|
||||
Update `src/content/market/index.ts`:
|
||||
|
||||
- add optional dependencies:
|
||||
|
||||
```ts
|
||||
getAuthState?: () => Promise<AuthStateValue>;
|
||||
promptBatchName?: () => string | null;
|
||||
submitBatch?: (payload: BatchPayload) => Promise<unknown>;
|
||||
```
|
||||
|
||||
- default `getAuthState` to a `chrome.runtime.sendMessage({ type: "auth:get-state" })` wrapper
|
||||
- default `promptBatchName` to `() => window.prompt("请输入批次名称")`
|
||||
- default `submitBatch` to `createBatchSubmitClient(...).submitBatch`
|
||||
- add `onSubmitBatch` handler in `ensurePluginToolbar(...)`
|
||||
- behavior:
|
||||
- read target via `readToolbarExportTarget()`
|
||||
- prompt for batch name
|
||||
- cancel on `null`
|
||||
- reject blank names with `setToolbarExportStatus(toolbar, "请输入批次名称")`
|
||||
- collect records via existing `exportRecords(target)`
|
||||
- read auth state
|
||||
- build payload with `createBatchPayload(...)`
|
||||
- call `submitBatch(payload)`
|
||||
- show `批次提交成功` on success
|
||||
- show thrown message or `批次提交失败,请稍后重试` on failure
|
||||
|
||||
- [ ] **Step 4: Extend auth state only if the tests expose a gap**
|
||||
|
||||
If tests reveal missing fields, ensure `src/background/auth/state.ts` continues exposing:
|
||||
- `userInfo.sub`
|
||||
- `userInfo.name`
|
||||
- `resource`
|
||||
|
||||
If `src/shared/auth-messages.ts` needs typing support, update only types, not message kinds.
|
||||
|
||||
- [ ] **Step 5: Re-run the focused market controller tests to verify green**
|
||||
|
||||
Run: `npm test -- tests/market-content-entry.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: Commit the market controller slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add src/content/market/index.ts src/shared/auth-messages.ts src/background/auth/state.ts tests/market-content-entry.test.ts
|
||||
git commit -m "feat: wire market batch submission flow"
|
||||
```
|
||||
|
||||
## Task 5: Extend The Mock Server And Popup Debugging
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/mock-protected-api.mjs`
|
||||
- Modify: `tests/mock-protected-api.test.ts`
|
||||
- Modify: `src/popup/view.ts`
|
||||
- Modify: `tests/popup-entry.test.ts` only if needed for debug output wording
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: Write the failing mock batch endpoint tests**
|
||||
|
||||
Add cases to `tests/mock-protected-api.test.ts`:
|
||||
|
||||
```ts
|
||||
test("accepts a batch payload when a Bearer token is present", async () => {
|
||||
const server = createMockProtectedApiServer({ port: 0 });
|
||||
await server.start();
|
||||
|
||||
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: "王少卿",
|
||||
logtoUserId: "p7pdhhtde8kj",
|
||||
resource: "https://talent-search.intelligrow.cn"
|
||||
}),
|
||||
headers: {
|
||||
Authorization: "Bearer abc123",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
acceptedCount: 1,
|
||||
ok: true,
|
||||
source: "mock-batch-submit"
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the mock server tests to verify red**
|
||||
|
||||
Run: `npm test -- tests/mock-protected-api.test.ts`
|
||||
|
||||
Expected: FAIL because the batch endpoint does not exist yet
|
||||
|
||||
- [ ] **Step 3: Implement the minimal mock batch endpoint**
|
||||
|
||||
Update `scripts/mock-protected-api.mjs`:
|
||||
|
||||
- keep the existing `/api/mock/protected`
|
||||
- add `/api/mock/batches` for `POST`
|
||||
- require the same Bearer header validation
|
||||
- read the JSON body and return:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"source": "mock-batch-submit",
|
||||
"acceptedCount": 1,
|
||||
"batchId": "<echoed batch id>"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update the docs**
|
||||
|
||||
Add to `README.md`:
|
||||
- how to trigger `提交批次`
|
||||
- how the prompt should be filled
|
||||
- expected mock response
|
||||
|
||||
- [ ] **Step 5: Re-run the mock server tests to verify green**
|
||||
|
||||
Run: `npm test -- tests/mock-protected-api.test.ts`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: Commit the mock batch slice**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/mock-protected-api.mjs tests/mock-protected-api.test.ts README.md
|
||||
git commit -m "feat: add mock batch submit endpoint"
|
||||
```
|
||||
|
||||
## Final Verification
|
||||
|
||||
- [ ] Run the focused batch feature tests:
|
||||
|
||||
```bash
|
||||
npm test -- tests/auth-config.test.ts tests/batch-payload.test.ts tests/batch-submit-client.test.ts tests/mock-protected-api.test.ts tests/market-content-entry.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] Run the full test suite:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] Run the production build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: build completes and updates `dist/`
|
||||
|
||||
- [ ] Perform the manual smoke test:
|
||||
|
||||
1. Start `npm run mock:protected-api`
|
||||
2. Reload the unpacked extension from `dist/`
|
||||
3. Log in through the popup
|
||||
4. Open the Xingtu market page
|
||||
5. Choose an export range
|
||||
6. Click `提交批次`
|
||||
7. Enter a batch name in `prompt()`
|
||||
8. Confirm success text appears
|
||||
9. Verify the mock response contains the batch id and accepted creator count
|
||||
Reference in New Issue
Block a user