Plan submit formatting button state
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
# Submit Formatting Button State 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:** Show `格式化中` on the submit button during automatic formatting, then show `正在提交` continuously while the submission request is pending.
|
||||
|
||||
**Architecture:** Extract the button presentation rules into a small pure TypeScript function so the state priority can be tested without adding a frontend test framework. Keep formatter and submission-request flags local to `SubmitCode.vue`, with `finally` blocks ensuring both flags clear on every outcome.
|
||||
|
||||
**Tech Stack:** Vue 3 Composition API, TypeScript, Node.js built-in test runner, Rsbuild
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Define and test submit button presentation rules
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/submitButtonState.test.ts`
|
||||
- Create: `src/oj/problem/components/submitButtonState.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/submitButtonState.test.ts`:
|
||||
|
||||
```ts
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import { getSubmitButtonState } from "../src/oj/problem/components/submitButtonState.ts"
|
||||
|
||||
const idleInput = {
|
||||
isAuthed: true,
|
||||
hasCode: true,
|
||||
isFormatting: false,
|
||||
isSubmitting: false,
|
||||
isJudging: false,
|
||||
isCooldown: false,
|
||||
}
|
||||
|
||||
test("shows a disabled loading state while formatting", () => {
|
||||
assert.deepEqual(
|
||||
getSubmitButtonState({ ...idleInput, isFormatting: true }),
|
||||
{
|
||||
disabled: true,
|
||||
label: "格式化中",
|
||||
icon: "eos-icons:loading",
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test("shows submitting immediately after formatting", () => {
|
||||
assert.deepEqual(
|
||||
getSubmitButtonState({ ...idleInput, isSubmitting: true }),
|
||||
{
|
||||
disabled: true,
|
||||
label: "正在提交",
|
||||
icon: "eos-icons:loading",
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test("preserves existing login, judging, cooldown, and idle states", () => {
|
||||
assert.deepEqual(
|
||||
getSubmitButtonState({ ...idleInput, isAuthed: false }),
|
||||
{
|
||||
disabled: true,
|
||||
label: "请先登录",
|
||||
icon: "ph:play-fill",
|
||||
},
|
||||
)
|
||||
assert.deepEqual(getSubmitButtonState({ ...idleInput, isJudging: true }), {
|
||||
disabled: true,
|
||||
label: "正在评分",
|
||||
icon: "eos-icons:loading",
|
||||
})
|
||||
assert.deepEqual(getSubmitButtonState({ ...idleInput, isCooldown: true }), {
|
||||
disabled: true,
|
||||
label: "正在冷却",
|
||||
icon: "ph:lightbulb-fill",
|
||||
})
|
||||
assert.deepEqual(getSubmitButtonState(idleInput), {
|
||||
disabled: false,
|
||||
label: "提交代码",
|
||||
icon: "ph:play-fill",
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/submitButtonState.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL with `ERR_MODULE_NOT_FOUND` for `submitButtonState.ts`.
|
||||
|
||||
- [ ] **Step 3: Implement the pure state function**
|
||||
|
||||
Create `src/oj/problem/components/submitButtonState.ts`:
|
||||
|
||||
```ts
|
||||
export interface SubmitButtonStateInput {
|
||||
isAuthed: boolean
|
||||
hasCode: boolean
|
||||
isFormatting: boolean
|
||||
isSubmitting: boolean
|
||||
isJudging: boolean
|
||||
isCooldown: boolean
|
||||
}
|
||||
|
||||
export interface SubmitButtonState {
|
||||
disabled: boolean
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export function getSubmitButtonState({
|
||||
isAuthed,
|
||||
hasCode,
|
||||
isFormatting,
|
||||
isSubmitting,
|
||||
isJudging,
|
||||
isCooldown,
|
||||
}: SubmitButtonStateInput): SubmitButtonState {
|
||||
const disabled =
|
||||
!isAuthed ||
|
||||
!hasCode ||
|
||||
isFormatting ||
|
||||
isSubmitting ||
|
||||
isJudging ||
|
||||
isCooldown
|
||||
|
||||
let label = "提交代码"
|
||||
if (!isAuthed) {
|
||||
label = "请先登录"
|
||||
} else if (isFormatting) {
|
||||
label = "格式化中"
|
||||
} else if (isSubmitting) {
|
||||
label = "正在提交"
|
||||
} else if (isJudging) {
|
||||
label = "正在评分"
|
||||
} else if (isCooldown) {
|
||||
label = "正在冷却"
|
||||
}
|
||||
|
||||
const icon =
|
||||
isFormatting || isSubmitting || isJudging
|
||||
? "eos-icons:loading"
|
||||
: isCooldown
|
||||
? "ph:lightbulb-fill"
|
||||
: "ph:play-fill"
|
||||
|
||||
return { disabled, label, icon }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/submitButtonState.test.ts
|
||||
```
|
||||
|
||||
Expected: 3 tests pass.
|
||||
|
||||
### Task 2: Connect formatting and submission request lifecycle to the button
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/oj/problem/components/SubmitCode.vue`
|
||||
|
||||
- [ ] **Step 1: Add local request states and computed presentation**
|
||||
|
||||
Import `getSubmitButtonState`, add `isFormatting` and `isSubmittingRequest` refs, and replace the three existing button computed properties with:
|
||||
|
||||
```ts
|
||||
const buttonState = computed(() =>
|
||||
getSubmitButtonState({
|
||||
isAuthed: userStore.isAuthed,
|
||||
hasCode: codeStore.code.value.trim() !== "",
|
||||
isFormatting: isFormatting.value,
|
||||
isSubmitting: isSubmittingRequest.value || submitting.value,
|
||||
isJudging: judging.value || pending.value,
|
||||
isCooldown: isCooldown.value,
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Use `buttonState.disabled`, `buttonState.icon`, and `buttonState.label` in the template.
|
||||
|
||||
- [ ] **Step 2: Guard and track the formatting request**
|
||||
|
||||
At the start of `submit`, return when `buttonState.value.disabled` is true. Around `formatCode`, set `isFormatting.value = true` before the request and clear it in `finally`:
|
||||
|
||||
```ts
|
||||
isFormatting.value = true
|
||||
try {
|
||||
const res = await formatCode({
|
||||
code: codeStore.code.value,
|
||||
language: formatLang,
|
||||
})
|
||||
codeStore.setCode(res.data.code)
|
||||
} catch (e: any) {
|
||||
if (e?.error === "format-error") {
|
||||
message.warning(`代码格式化失败:${e.data},请检查代码后重试`)
|
||||
return
|
||||
}
|
||||
} finally {
|
||||
isFormatting.value = false
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Track the submission API request**
|
||||
|
||||
Set `isSubmittingRequest.value = true` immediately before `submitCode`, keep the existing success flow inside the `try`, and clear the request state in `finally`:
|
||||
|
||||
```ts
|
||||
isSubmittingRequest.value = true
|
||||
try {
|
||||
const res = await submitCode(data)
|
||||
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
|
||||
|
||||
startCooldown()
|
||||
startMonitoring(res.data.submission_id)
|
||||
showResult.value = true
|
||||
} finally {
|
||||
isSubmittingRequest.value = false
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test tests/submitButtonState.test.ts
|
||||
```
|
||||
|
||||
Expected: 3 tests pass.
|
||||
|
||||
- [ ] **Step 5: Run the production build**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: Rsbuild exits with status 0.
|
||||
|
||||
- [ ] **Step 6: Check the final diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
git diff -- src/oj/problem/components/SubmitCode.vue src/oj/problem/components/submitButtonState.ts tests/submitButtonState.test.ts
|
||||
```
|
||||
|
||||
Expected: no whitespace errors; diff is limited to the button state feature and its test.
|
||||
Reference in New Issue
Block a user