fix(review): persist uploaded .tex file in reformat panel

- File now stored in edu.js store (reformatFile)
- Switching target journal no longer requires re-upload
- Explicit reset button clears file
- Vitest: 4 cases
This commit is contained in:
potaser
2026-10-13 11:20:33 +08:00
parent 3a72d3a861
commit da84dff9bb
3 changed files with 106 additions and 2 deletions
@@ -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);
});
});
@@ -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';
}
+3
View File
@@ -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,