summaryrefslogtreecommitdiff
path: root/frontend/src/lib/strategyTab.test.ts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
commit5c0ba37eda80d39e6ceca59bb1d5f4942f858995 (patch)
tree948723f9cedf7ccb0707fa6ee516bd30fe20fd10 /frontend/src/lib/strategyTab.test.ts
downloadstrategy-lab-5c0ba37eda80d39e6ceca59bb1d5f4942f858995.tar.gz
chore: establish Strategy Lab source baseline (development, not release)
Diffstat (limited to 'frontend/src/lib/strategyTab.test.ts')
-rw-r--r--frontend/src/lib/strategyTab.test.ts95
1 files changed, 95 insertions, 0 deletions
diff --git a/frontend/src/lib/strategyTab.test.ts b/frontend/src/lib/strategyTab.test.ts
new file mode 100644
index 0000000..a5bf6d8
--- /dev/null
+++ b/frontend/src/lib/strategyTab.test.ts
@@ -0,0 +1,95 @@
+// @ts-nocheck
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
+import { mount, unmount } from 'svelte';
+// NOTE: `resolve.conditions: ['browser']` lets vitest resolve svelte's client
+// build so actual component mounting works in jsdom.
+import { putDraft } from '../lib/client';
+import { ApiError } from '../lib/api';
+
+// Live regression (artifacts/qa/supervisor-live/browser-strategy.json):
+// ProjectStrategyTab rendered `c(...).slice is not a function` pageerror and a
+// blank main because the backend sends draft_generation as a NUMBER (i64) while
+// the frontend typed it as a string and called .slice on it. Backend keeps the
+// integer; these tests exercise the ACTUAL numeric API shape through the real
+// fetch client and the REAL component render in jsdom (no source-string-only
+// assertions).
+
+const PROJECT_ID = '7630e296-0b2f-44c3-85f1-fe695e7a7e4b';
+
+const json = (v, status = 200) =>
+ new Response(JSON.stringify(v), { status, headers: { 'content-type': 'application/json' } });
+
+function projectBody(generation) {
+ return {
+ id: PROJECT_ID, name: '端到端验收项目', description: null,
+ draft_code: 'import backtrader as bt\nclass Strategy(bt.Strategy):\n pass\n',
+ draft_generation: generation, // NUMERIC contract from the backend (i64)
+ created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
+ };
+}
+
+async function mountStrategyTab(fetchImpl) {
+ vi.stubGlobal('fetch', fetchImpl);
+ const ProjectStrategyTab = (await import('../pages/ProjectStrategyTab.svelte')).default;
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const inst = mount(ProjectStrategyTab, { target: host, props: { projectId: PROJECT_ID } });
+ // flush effects/microtasks
+ await new Promise((r) => setTimeout(r, 50));
+ return { host, inst, cleanup: () => { try { unmount(inst); } catch {} host.remove(); vi.unstubAllGlobals(); } };
+}
+
+function handler() {
+ return (url, _opts = {}) => {
+ if (/\/api\/projects\/[0-9a-f-]+$/.test(String(url)))
+ return Promise.resolve(json(projectBody(3)));
+ if (/\/versions/.test(String(url)))
+ return Promise.resolve(json({ items: [] }));
+ return Promise.resolve(json({}));
+ };
+}
+
+describe('strategy tab numeric generation contract (pageerror regression)', () => {
+ beforeEach(() => { document.body.innerHTML = ''; });
+ afterEach(() => vi.unstubAllGlobals());
+
+ it('full numeric flow renders editor + 草稿代 3 without .slice crash', async () => {
+ const { host, cleanup } = await mountStrategyTab(handler());
+ try {
+ const text = host.textContent;
+ // editor content from the numeric flow (component reached rendering)
+ expect(text).toContain('class Strategy(bt.Strategy)');
+ // generation renders as the actual number, never a truncated-slice crash
+ expect(text).toMatch(/草稿代\s*3(?:\s|$|·)/);
+ expect(text).not.toContain('slice is not a function');
+ } finally { cleanup(); }
+ });
+
+ it('client PUT sends numeric expected_generation and accepts numeric response', async () => {
+ const seen = [];
+ vi.stubGlobal('fetch', (url, opts = {}) => {
+ seen.push([String(url), opts]);
+ return Promise.resolve(json({ id: PROJECT_ID, draft_code: 'x=1', draft_generation: 4 }));
+ });
+ const res = await putDraft(PROJECT_ID, 'x=1', 3);
+ expect(res.draft_generation).toBe(4);
+ expect(typeof res.draft_generation).toBe('number');
+ const [, sentOpts] = seen[0];
+ const sentBody = typeof sentOpts.body === 'string' ? JSON.parse(sentOpts.body) : sentOpts.body;
+ expect(sentBody.expected_generation).toBe(3);
+ expect(typeof sentBody.expected_generation).toBe('number');
+ });
+
+ it('409 conflict keeps stale_generation code (numeric retry flow preserved)', async () => {
+ vi.stubGlobal('fetch', (url) => {
+ if (/\/draft$/.test(String(url)))
+ return Promise.resolve(new Response(JSON.stringify({ error: { code: 'stale_generation', message: 'g3' } }), { status: 409, headers: { 'content-type': 'application/json' } }));
+ return Promise.resolve(json({}));
+ });
+ let err;
+ await putDraft(PROJECT_ID, 'stale', 2).catch((e) => { err = e; });
+ expect(err).toBeInstanceOf(ApiError);
+ expect(err.status).toBe(409);
+ expect(err.code).toBe('stale_generation');
+ });
+});