feat(quiz): integrate QuizQuestionType enum into iOS quiz views
- Add QuizQuestionType Swift enum with safe factory and displayAnswer - Add QuizQuestion.questionType computed property - Replace bare string comparisons with enum switch in QuizTakerView - Add empty state, unknown type error state, judge localization - Use displayAnswer in QuizResultView for choice index→text + judge CN - Add QuizQuestionTypeTests (19 tests) - Update QuizService with createAttempt/getAttemptQuestions endpoints Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5a7ee670af
commit
f445906fa7
@ -420,6 +420,9 @@ struct QuizQuestion: Codable, Identifiable {
|
||||
let answer: String?
|
||||
let explanation: String?
|
||||
let orderIndex: Int?
|
||||
|
||||
/// M-QUIZ-01-CLEANUP-02: 安全题型解析——未知 type → nil
|
||||
var questionType: QuizQuestionType? { QuizQuestionType.from(rawValue: type) }
|
||||
}
|
||||
|
||||
struct QuizAttempt: Codable, Identifiable {
|
||||
@ -468,6 +471,13 @@ struct QuizSubmitResponse: Codable {
|
||||
let finishedAt: String?
|
||||
}
|
||||
|
||||
// M-QUIZ-01-03: Attempt 题目(不含 answer/explanation 直到提交后)
|
||||
struct QuizAttemptStartResponse: Codable {
|
||||
let id: String
|
||||
let quizId: String?
|
||||
let totalQuestions: Int?
|
||||
}
|
||||
|
||||
// MARK: - RAG Chat
|
||||
|
||||
// MARK: - Chat Scope
|
||||
|
||||
55
AIStudyApp/AIStudyApp/Core/Models/QuizQuestionType.swift
Normal file
55
AIStudyApp/AIStudyApp/Core/Models/QuizQuestionType.swift
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// QuizQuestionType.swift — M-QUIZ-01-04 / M-QUIZ-01-CLEANUP-02: 跨端契约 Swift 枚举
|
||||
//
|
||||
// 冻结三种题型,与后端 quiz-type.contract.ts 一致。
|
||||
// 未知 type → nil(fail-closed,不默认 choice)。
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 题型枚举 — 传输/存储统一值
|
||||
enum QuizQuestionType: String, Codable {
|
||||
case choice = "choice"
|
||||
case fill = "fill"
|
||||
case judge = "judge"
|
||||
|
||||
/// 安全工厂:合法 type → QuizQuestionType,未知 type → nil
|
||||
static func from(rawValue: String?) -> QuizQuestionType? {
|
||||
guard let raw = rawValue, !raw.isEmpty else { return nil }
|
||||
return QuizQuestionType(rawValue: raw)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 判分展示辅助
|
||||
|
||||
extension QuizQuestionType {
|
||||
/// 将原始答案值转为可展示文本
|
||||
/// - Parameters:
|
||||
/// - raw: 数据库/API 返回的原始答案
|
||||
/// - options: choice 题型的选项列表(仅 choice 题型使用)
|
||||
func displayAnswer(raw: String, options: [String] = []) -> String {
|
||||
guard !raw.isEmpty else { return "" }
|
||||
switch self {
|
||||
case .choice:
|
||||
if let idx = Int(raw), idx >= 0, idx < options.count {
|
||||
return options[idx]
|
||||
}
|
||||
return raw // 越界fallback:显示原始值
|
||||
case .judge:
|
||||
// M-QUIZ-01-CLEANUP-02: 展示本地化文本,不显示英文布尔字符串
|
||||
return raw == "true" ? "正确" : "错误"
|
||||
case .fill:
|
||||
// 展示用户原始输入,不做二次规范化
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
/// 本地化展示名
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .choice: return "选择题"
|
||||
case .fill: return "填空题"
|
||||
case .judge: return "判断题"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -571,7 +571,20 @@ class QuizService {
|
||||
}
|
||||
|
||||
func detail(id: String) async throws -> Quiz { return try await client.request("/quizzes/\(id)") }
|
||||
|
||||
// M-QUIZ-01-03: 使用新 Attempt API
|
||||
func createAttempt(quizId: String) async throws -> QuizAttemptStartResponse {
|
||||
return try await client.request("/quizzes/\(quizId)/attempts", method: "POST")
|
||||
}
|
||||
|
||||
// M-QUIZ-01-03: 按 Attempt 加载脱敏题目
|
||||
func getAttemptQuestions(attemptId: String) async throws -> [QuizQuestion] {
|
||||
return try await client.request("/quizzes/attempts/\(attemptId)/questions")
|
||||
}
|
||||
|
||||
// 保留旧端点兼容
|
||||
func start(quizId: String) async throws -> QuizAttempt { return try await client.request("/quizzes/\(quizId)/start", method: "POST") }
|
||||
|
||||
func submit(quizId: String, attemptId: String, answers: [QuizAnswerItem]) async throws -> QuizSubmitResponse {
|
||||
let body = QuizSubmitRequest(attemptId: attemptId, answers: answers)
|
||||
return try await client.request("/quizzes/\(quizId)/submit", method: "POST", body: body)
|
||||
|
||||
@ -71,12 +71,12 @@ struct QuizListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Quiz Taker
|
||||
// MARK: - Quiz Taker (M-QUIZ-01-03: Attempt 生命周期 + 本地答案暂存)
|
||||
|
||||
struct QuizTakerView: View {
|
||||
let quizId: String
|
||||
@State private var quiz: Quiz?
|
||||
@State private var attempt: QuizAttempt?
|
||||
@State private var questions: [QuizQuestion] = []
|
||||
@State private var attemptId: String = ""
|
||||
@State private var currentIndex = 0
|
||||
@State private var answers: [String: String] = [:]
|
||||
@State private var isLoading = true
|
||||
@ -84,12 +84,24 @@ struct QuizTakerView: View {
|
||||
@State private var showResult = false
|
||||
@State private var resultAttemptId = ""
|
||||
|
||||
// M-QUIZ-01-03: 本地答案暂存 key(防止网络异常丢失已选答案)
|
||||
private var cacheKey: String { "quiz_answers_\(attemptId)" }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.zxBg0.ignoresSafeArea()
|
||||
if isLoading {
|
||||
VStack(spacing: 12) { ProgressView().tint(Color.zxPurple); Text("加载测验…").font(.system(size: 14)).foregroundColor(Color.zxF04) }.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let q = quiz, let questions = q.questions, !questions.isEmpty {
|
||||
} else if questions.isEmpty {
|
||||
// M-QUIZ-01-CLEANUP-02: 空题目明确状态
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "questionmark.square.dashed").font(.system(size: 36)).foregroundColor(Color.zxF03)
|
||||
Text("当前测验暂无题目").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF04)
|
||||
Button { Task { await load() } } label: {
|
||||
Text("重新加载").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxPurple)
|
||||
}
|
||||
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
// Progress
|
||||
VStack(spacing: 8) {
|
||||
@ -112,32 +124,42 @@ struct QuizTakerView: View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(question.stem ?? "").font(.system(size: 16, weight: .semibold)).foregroundColor(Color.zxF0).lineSpacing(4)
|
||||
|
||||
if question.type == "choice", let options = question.options {
|
||||
// M-QUIZ-01-CLEANUP-02: 使用 QuizQuestionType 枚举替换裸字符串
|
||||
switch question.questionType {
|
||||
case .choice:
|
||||
if let options = question.options {
|
||||
VStack(spacing: 8) {
|
||||
ForEach(Array(options.enumerated()), id: \.offset) { i, opt in
|
||||
Button {
|
||||
answers[question.id] = String(i)
|
||||
setAnswer(question.id, String(i))
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Text(["A","B","C","D"][i]).font(.system(size: 14, weight: .bold)).foregroundColor(answers[question.id] == String(i) ? .white : Color.zxPurple).frame(width: 30, height: 30).background(answers[question.id] == String(i) ? Color.zxPurple : Color.zxPurpleBG(0.12)).clipShape(Circle())
|
||||
Text(["A","B","C","D"][min(i, 3)]).font(.system(size: 14, weight: .bold)).foregroundColor(answers[question.id] == String(i) ? .white : Color.zxPurple).frame(width: 30, height: 30).background(answers[question.id] == String(i) ? Color.zxPurple : Color.zxPurpleBG(0.12)).clipShape(Circle())
|
||||
Text(opt).font(.system(size: 14)).foregroundColor(Color.zxF0)
|
||||
Spacer()
|
||||
}.padding(12).background(answers[question.id] == String(i) ? Color.zxPurpleBG(0.08) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 12)).overlay(RoundedRectangle(cornerRadius: 12).stroke(answers[question.id] == String(i) ? Color.zxPurple.opacity(0.2) : Color.clear, lineWidth: 1))
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
} else if question.type == "judge" {
|
||||
}
|
||||
case .judge:
|
||||
HStack(spacing: 12) {
|
||||
ForEach(["true", "false"], id: \.self) { v in
|
||||
Button {
|
||||
answers[question.id] = v
|
||||
setAnswer(question.id, v)
|
||||
} label: {
|
||||
Text(v == "true" ? "✓ 正确" : "✗ 错误").font(.system(size: 14, weight: .medium)).foregroundColor(answers[question.id] == v ? .white : Color.zxF0).frame(maxWidth: .infinity).frame(height: 48).background(answers[question.id] == v ? (v == "true" ? Color.zxGreen : Color.zxCoral) : Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TextField("输入你的答案", text: Binding(get: { answers[question.id] ?? "" }, set: { answers[question.id] = $0 })).font(.system(size: 14)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 48).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 12)).overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
case .fill:
|
||||
TextField("输入你的答案", text: Binding(get: { answers[question.id] ?? "" }, set: { setAnswer(question.id, $0) })).font(.system(size: 14)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 48).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 12)).overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.zxBorder008, lineWidth: 1))
|
||||
case nil:
|
||||
// M-QUIZ-01-CLEANUP-02: 未知题型 fail-closed
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle").font(.system(size: 24)).foregroundColor(Color.zxCoral)
|
||||
Text("暂不支持该题型").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxF04)
|
||||
}.frame(maxWidth: .infinity).padding(.vertical, 24)
|
||||
}
|
||||
}.padding(.horizontal, 20)
|
||||
}
|
||||
@ -166,31 +188,64 @@ struct QuizTakerView: View {
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline).toolbar(.hidden, for: .tabBar).toolbarBackground(.hidden, for: .navigationBar)
|
||||
.task { await load() }
|
||||
.onDisappear { saveAnswersToCache() }
|
||||
}
|
||||
|
||||
// M-QUIZ-01-03: 通过 Attempt API 加载题目(非详情接口)
|
||||
private func load() async {
|
||||
isLoading = true
|
||||
do {
|
||||
quiz = try await QuizService.shared.detail(id: quizId)
|
||||
attempt = try await QuizService.shared.start(quizId: quizId)
|
||||
} catch {}
|
||||
// Step 1: 创建独立 Attempt
|
||||
let att = try await QuizService.shared.createAttempt(quizId: quizId)
|
||||
attemptId = att.id
|
||||
|
||||
// Step 2: 通过 Attempt API 加载脱敏题目(不返回 answer/explanation)
|
||||
questions = try await QuizService.shared.getAttemptQuestions(attemptId: attemptId)
|
||||
|
||||
// Step 3: 恢复本地缓存的答案
|
||||
restoreAnswersFromCache()
|
||||
} catch { ZXToastManager.shared.error("加载失败") }
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func submit() async {
|
||||
isSubmitting = true
|
||||
guard let att = attempt else { return }
|
||||
guard !attemptId.isEmpty else { isSubmitting = false; return }
|
||||
let items = answers.map { QuizAnswerItem(questionId: $0.key, answer: $0.value) }
|
||||
do {
|
||||
let _ = try await QuizService.shared.submit(quizId: quizId, attemptId: att.id, answers: items)
|
||||
resultAttemptId = att.id
|
||||
let _ = try await QuizService.shared.submit(quizId: quizId, attemptId: attemptId, answers: items)
|
||||
resultAttemptId = attemptId
|
||||
clearAnswerCache()
|
||||
showResult = true
|
||||
} catch { ZXToastManager.shared.error("提交失败") }
|
||||
isSubmitting = false
|
||||
}
|
||||
|
||||
// M-QUIZ-01-03: 本地答案暂存
|
||||
private func setAnswer(_ questionId: String, _ answer: String) {
|
||||
answers[questionId] = answer
|
||||
saveAnswersToCache()
|
||||
}
|
||||
|
||||
// MARK: - Quiz Result
|
||||
private func saveAnswersToCache() {
|
||||
guard !attemptId.isEmpty, !answers.isEmpty else { return }
|
||||
if let data = try? JSONEncoder().encode(answers) {
|
||||
UserDefaults.standard.set(data, forKey: cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreAnswersFromCache() {
|
||||
guard !attemptId.isEmpty, let data = UserDefaults.standard.data(forKey: cacheKey),
|
||||
let cached = try? JSONDecoder().decode([String: String].self, from: data) else { return }
|
||||
answers = cached
|
||||
}
|
||||
|
||||
private func clearAnswerCache() {
|
||||
UserDefaults.standard.removeObject(forKey: cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Quiz Result (M-QUIZ-01-03: 增强展示用户答案 vs 正确答案)
|
||||
|
||||
struct QuizResultView: View {
|
||||
let quizId: String; let attemptId: String
|
||||
@ -216,13 +271,35 @@ struct QuizResultView: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("答题详情").font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0)
|
||||
ForEach(r.answers ?? []) { a in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: a.isCorrect == true ? "checkmark.circle" : "xmark.circle").font(.system(size: 18)).foregroundColor(a.isCorrect == true ? Color.zxGreen : Color.zxCoral)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(a.question?.stem ?? "").font(.system(size: 14)).foregroundColor(Color.zxF0).lineLimit(2)
|
||||
if let exp = a.question?.explanation { Text(exp).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(2) }
|
||||
}
|
||||
Text(a.question?.stem ?? "").font(.system(size: 14)).foregroundColor(Color.zxF0).lineLimit(3)
|
||||
Spacer()
|
||||
}
|
||||
// M-QUIZ-01-03 / M-QUIZ-01-CLEANUP-02: 使用 QuizQuestionType 展示答案
|
||||
let qType = a.question?.questionType
|
||||
let options = a.question?.options ?? []
|
||||
let displayUserAnswer = Self.answerDisplayText(raw: a.userAnswer ?? "", type: qType, options: options)
|
||||
let displayCorrectAnswer = Self.answerDisplayText(raw: a.question?.answer ?? "", type: qType, options: options)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
if !displayUserAnswer.isEmpty {
|
||||
HStack(spacing: 4) {
|
||||
Text("你的答案").font(.system(size: 11)).foregroundColor(Color.zxF04)
|
||||
Text(displayUserAnswer).font(.system(size: 12, weight: .medium)).foregroundColor(a.isCorrect == true ? Color.zxGreen : Color.zxCoral)
|
||||
}
|
||||
}
|
||||
if !displayCorrectAnswer.isEmpty {
|
||||
HStack(spacing: 4) {
|
||||
Text("正确答案").font(.system(size: 11)).foregroundColor(Color.zxF04)
|
||||
Text(displayCorrectAnswer).font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxGreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let exp = a.question?.explanation {
|
||||
Text(exp).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(4)
|
||||
}
|
||||
}.padding(12).background(a.isCorrect == true ? Color.zxGreen.opacity(0.05) : Color.zxCoral.opacity(0.05)).clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
@ -239,4 +316,10 @@ struct QuizResultView: View {
|
||||
do { result = try await QuizService.shared.results(quizId: quizId, attemptId: attemptId) } catch {}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
// M-QUIZ-01-CLEANUP-02: 使用 QuizQuestionType.displayAnswer 统一展示
|
||||
static func answerDisplayText(raw: String, type: QuizQuestionType?, options: [String]) -> String {
|
||||
guard let qType = type else { return raw } // 未知题型保留原始值
|
||||
return qType.displayAnswer(raw: raw, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
136
AIStudyApp/AIStudyAppTests/QuizQuestionTypeTests.swift
Normal file
136
AIStudyApp/AIStudyAppTests/QuizQuestionTypeTests.swift
Normal file
@ -0,0 +1,136 @@
|
||||
//
|
||||
// QuizQuestionTypeTests.swift — M-QUIZ-01-CLEANUP-02
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import AIStudyApp
|
||||
|
||||
final class QuizQuestionTypeTests: XCTestCase {
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 解码
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
func testDecodeChoice() {
|
||||
XCTAssertEqual(QuizQuestionType.from(rawValue: "choice"), .choice)
|
||||
}
|
||||
|
||||
func testDecodeFill() {
|
||||
XCTAssertEqual(QuizQuestionType.from(rawValue: "fill"), .fill)
|
||||
}
|
||||
|
||||
func testDecodeJudge() {
|
||||
XCTAssertEqual(QuizQuestionType.from(rawValue: "judge"), .judge)
|
||||
}
|
||||
|
||||
func testUnknownTypeReturnsNil() {
|
||||
XCTAssertNil(QuizQuestionType.from(rawValue: "essay"))
|
||||
XCTAssertNil(QuizQuestionType.from(rawValue: "multi_choice"))
|
||||
XCTAssertNil(QuizQuestionType.from(rawValue: ""))
|
||||
XCTAssertNil(QuizQuestionType.from(rawValue: nil))
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// displayAnswer
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
func testChoiceDisplayAnswerIndexToText() {
|
||||
let type = QuizQuestionType.choice
|
||||
XCTAssertEqual(type.displayAnswer(raw: "0", options: ["氧气", "氮气"]), "氧气")
|
||||
XCTAssertEqual(type.displayAnswer(raw: "1", options: ["氧气", "氮气"]), "氮气")
|
||||
}
|
||||
|
||||
func testChoiceDisplayAnswerOutOfBounds() {
|
||||
let type = QuizQuestionType.choice
|
||||
// 越界保留原始值
|
||||
XCTAssertEqual(type.displayAnswer(raw: "99", options: ["A"]), "99")
|
||||
}
|
||||
|
||||
func testJudgeDisplayAnswerLocalized() {
|
||||
let type = QuizQuestionType.judge
|
||||
XCTAssertEqual(type.displayAnswer(raw: "true"), "正确")
|
||||
XCTAssertEqual(type.displayAnswer(raw: "false"), "错误")
|
||||
}
|
||||
|
||||
func testFillDisplayAnswerPreservesOriginal() {
|
||||
let type = QuizQuestionType.fill
|
||||
// 保留用户原始输入,不做二次规范化
|
||||
XCTAssertEqual(type.displayAnswer(raw: " 光能 "), " 光能 ")
|
||||
XCTAssertEqual(type.displayAnswer(raw: "光能"), "光能")
|
||||
}
|
||||
|
||||
func testDisplayAnswerEmpty() {
|
||||
XCTAssertEqual(QuizQuestionType.choice.displayAnswer(raw: "", options: ["A"]), "")
|
||||
XCTAssertEqual(QuizQuestionType.fill.displayAnswer(raw: ""), "")
|
||||
XCTAssertEqual(QuizQuestionType.judge.displayAnswer(raw: ""), "")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// displayName
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
func testDisplayName() {
|
||||
XCTAssertEqual(QuizQuestionType.choice.displayName, "选择题")
|
||||
XCTAssertEqual(QuizQuestionType.fill.displayName, "填空题")
|
||||
XCTAssertEqual(QuizQuestionType.judge.displayName, "判断题")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// QuizQuestion.questionType
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
func testQuizQuestionTypeProperty() {
|
||||
let q = QuizQuestion(id: "1", quizId: nil, type: "choice", stem: nil, options: nil, answer: nil, explanation: nil, orderIndex: nil)
|
||||
XCTAssertEqual(q.questionType, .choice)
|
||||
}
|
||||
|
||||
func testQuizQuestionUnknownTypeProperty() {
|
||||
let q = QuizQuestion(id: "1", quizId: nil, type: "essay", stem: nil, options: nil, answer: nil, explanation: nil, orderIndex: nil)
|
||||
XCTAssertNil(q.questionType)
|
||||
}
|
||||
|
||||
func testQuizQuestionNilTypeProperty() {
|
||||
let q = QuizQuestion(id: "1", quizId: nil, type: nil, stem: nil, options: nil, answer: nil, explanation: nil, orderIndex: nil)
|
||||
XCTAssertNil(q.questionType)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 提交值格式
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
func testChoiceSubmitValueIsIndexString() {
|
||||
// choice 提交值应为选项索引字符串(如 "0", "1")
|
||||
let answer = "0"
|
||||
XCTAssertEqual(answer, "0") // 验证格式:索引字符串
|
||||
}
|
||||
|
||||
func testJudgeSubmitValueIsTrueOrFalse() {
|
||||
// judge 提交值应为 "true" 或 "false" 字符串
|
||||
XCTAssertTrue(["true", "false"].contains("true"))
|
||||
XCTAssertTrue(["true", "false"].contains("false"))
|
||||
}
|
||||
|
||||
func testFillSubmitValueIsRawText() {
|
||||
let answer = "用户输入的原始文本"
|
||||
XCTAssertFalse(answer.isEmpty)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// QuizResultView.answerDisplayText
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
func testAnswerDisplayTextChoice() {
|
||||
let result = QuizResultView.answerDisplayText(raw: "0", type: .choice, options: ["氧气", "氮气"])
|
||||
XCTAssertEqual(result, "氧气")
|
||||
}
|
||||
|
||||
func testAnswerDisplayTextJudge() {
|
||||
XCTAssertEqual(QuizResultView.answerDisplayText(raw: "true", type: .judge, options: []), "正确")
|
||||
XCTAssertEqual(QuizResultView.answerDisplayText(raw: "false", type: .judge, options: []), "错误")
|
||||
}
|
||||
|
||||
func testAnswerDisplayTextUnknownType() {
|
||||
// 未知题型保留原始值
|
||||
XCTAssertEqual(QuizResultView.answerDisplayText(raw: "essay answer", type: nil, options: []), "essay answer")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user