diff --git a/tests/frontend/unit/edu/reformat.file.test.js b/tests/frontend/unit/edu/reformat.file.test.js new file mode 100644 index 0000000..e8cd43b --- /dev/null +++ b/tests/frontend/unit/edu/reformat.file.test.js @@ -0,0 +1,99 @@ +// tests/frontend/unit/edu/reformat.file.test.js —— ReformatPanel 文件持久化(P3b 修复) +// bug:上传 .tex → 重排版 → 点"重新排版"换期刊 → 本地 file 被清空 → 要求重传。 +// 修复:File 进 store(reformatFile,同 presubmitFile 模式);reset() 不清 file; +// 仅"×" removeFile 清空。 +// 1) store 有 reformatFile → 挂载即回显文件名 +// 2) 选择文件 → 切换目标期刊 → 文件仍在(store + DOM) +// 3) 有结果时点"重新排版"(reset)→ 回上传区且文件保留 +// 4) "×" 移除 → store + 本地都清空 +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createPinia, setActivePinia } from 'pinia'; +import ReformatPanel from '../../../../ui/src/components/edu/panels/ReformatPanel.vue'; + +vi.mock('../../../../ui/src/api/review.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getJournals: vi.fn().mockResolvedValue({ + ok: true, + items: [{ id: 'gb', name: 'Genome Biology' }, { id: 'mp', name: 'Molecular Plant' }], + }), + getDirectory: vi.fn(), + reformatTex: vi.fn(), + compileTex: vi.fn(), + }; +}); + +import { useEduStore } from '../../../../ui/src/stores/edu.js'; + +const TEX = new File(['\\documentclass{article}\\begin{document}Hi\\end{document}'], 'paper.tex', { + type: 'application/x-tex', +}); + +/** 先建 pinia → seed store → 再挂载(保证 mount 时读到 store 初值) */ +function setup(seedFn) { + const pinia = createPinia(); + setActivePinia(pinia); + const edu = useEduStore(); + if (seedFn) seedFn(edu); + const wrapper = mount(ReformatPanel, { global: { plugins: [pinia] } }); + return { wrapper, edu }; +} + +describe('ReformatPanel 文件持久化(换期刊不丢文件)', () => { + beforeEach(() => { + global.fetch = vi.fn().mockRejectedValue(new Error('no network')); + if (!URL.createObjectURL) URL.createObjectURL = vi.fn(() => 'blob:mock'); + if (!URL.revokeObjectURL) URL.revokeObjectURL = vi.fn(); + }); + + it('1) store 有 reformatFile → 挂载即回显文件名', async () => { + const { wrapper } = setup((edu) => { edu.reformatFile = TEX; }); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.rf-file-name').text()).toContain('paper.tex'); + }); + + it('2) 选择文件后切目标期刊 → 文件仍在(store + DOM)', async () => { + const { wrapper, edu } = setup(); + const input = wrapper.find('input.rf-file-input'); + Object.defineProperty(input.element, 'files', { value: [TEX], configurable: true }); + await input.trigger('change'); + await wrapper.vm.$nextTick(); + expect(edu.reformatFile).toBe(TEX); + expect(wrapper.find('.rf-file-name').text()).toContain('paper.tex'); + // 切换目标期刊(watch 只写 drafts,不清 file) + await wrapper.find('#rf-target').setValue('mp'); + await wrapper.vm.$nextTick(); + expect(edu.reformatFile).toBe(TEX); + expect(wrapper.find('.rf-file-name').text()).toContain('paper.tex'); + expect(edu.drafts.reformatTarget).toBe('mp'); + }); + + it('3) 有结果时点"重新排版" → 回上传区且文件保留', async () => { + const { wrapper, edu } = setup((e) => { + e.reformatFile = TEX; + e.reformatResult = { + ok: true, job_id: 'abc123def456', modified_tex: 'x', changes: [], warnings: [], + meta: { filename: 'paper.tex' }, + }; + e.reformatStatus = 'ready'; + }); + await wrapper.vm.$nextTick(); + const again = wrapper.findAll('button').find((b) => b.text() === '重新排版'); + expect(again).toBeTruthy(); + await again.trigger('click'); + await wrapper.vm.$nextTick(); + expect(edu.reformatResult).toBeNull(); + expect(edu.reformatFile).toBe(TEX); // store 保留 + expect(wrapper.find('.rf-file-name').text()).toContain('paper.tex'); // DOM 回显 + }); + + it('4) "×" 移除 → store + 本地都清空', async () => { + const { wrapper, edu } = setup((e) => { e.reformatFile = TEX; }); + await wrapper.vm.$nextTick(); + await wrapper.find('.rf-remove').trigger('click'); + expect(edu.reformatFile).toBeNull(); + expect(wrapper.find('.rf-file-name').exists()).toBe(false); + }); +}); diff --git a/ui/src/components/edu/panels/ReformatPanel.vue b/ui/src/components/edu/panels/ReformatPanel.vue index 50a4f02..2b418fe 100644 --- a/ui/src/components/edu/panels/ReformatPanel.vue +++ b/ui/src/components/edu/panels/ReformatPanel.vue @@ -29,7 +29,7 @@ const SOURCE_LABEL = { }; const fileInput = ref(null); -const file = ref(null); +const file = ref(edu.reformatFile || null); // File 存 store(换期刊重排不丢;× 移除才清空) const fileError = ref(''); const dragging = ref(false); const tab = ref('source'); // 'source' | 'diff' | 'pdf' @@ -202,6 +202,7 @@ function acceptFile(f) { return; } file.value = f; + edu.reformatFile = f; // 进 store:换期刊/切菜单重排不丢 } function onPick(e) { acceptFile(e.target.files?.[0]); @@ -213,6 +214,7 @@ function onDrop(e) { } function removeFile() { file.value = null; + edu.reformatFile = null; // 唯一清空入口:"×" 移除 } async function submit() { @@ -225,7 +227,7 @@ async function submit() { } function reset() { edu.clearReformat(); - file.value = null; + // 不清 file——换期刊重排时保留已上传文件(仅"×"移除才清) fileError.value = ''; tab.value = 'source'; } diff --git a/ui/src/stores/edu.js b/ui/src/stores/edu.js index e9bfabd..b427cc8 100644 --- a/ui/src/stores/edu.js +++ b/ui/src/stores/edu.js @@ -72,6 +72,9 @@ export const useEduStore = defineStore('edu', { // ③ presubmit 已选 PDF(File 对象,仅内存:面板卸载/切菜单不丢,error 态可直接重试; // 不可序列化 → 不进 localStorage;clearReport 一并清空) presubmitFile: null, + // ⑤' reformat 已选 .tex(同 presubmitFile 模式:换期刊重排不丢文件; + // 面板内"×"移除才清空。File 不可序列化 → 不进 localStorage) + reformatFile: null, recommend: null, // ② 推荐响应 {ok, tiers, meta} recommendStatus: 'idle', recommendError: null,