Major changes: - Textbook UI: chapter list, reader, and interactive exercise view (keyboard + Apple Pencil) surfaced under the Course tab. 30 chapters, 251 exercises. - Stem-change conjugation toggle on Week 4 flashcard decks (E-IE, E-I, O-UE). Uses existing VerbForm + IrregularSpan data to render highlighted present tense conjugations inline. - Deterministic on-device answer grader with partial credit (correct / close for accent-stripped or single-char-typo / wrong). 11 unit tests cover it. - SharedModels: TextbookChapter (local), TextbookExerciseAttempt (cloud- synced), AnswerGrader helpers. Bumped schema. - DataLoader: textbook seeder (version 8) + refresh helpers that preserve LanGo course decks when textbook data is re-seeded. - Local extraction pipeline in Conjuga/Scripts/textbook/ — XHTML chapter parser, answer-key parser, macOS Vision image OCR + PDF page OCR, merger, NSSpellChecker validator, language-aware auto-fixer, and repair pass that re-pairs quarantined vocab rows using bounding-box coordinates. - UI test target (ConjugaUITests) with three tests: end-to-end textbook flow, all-chapters screenshot audit, and stem-change toggle verification. Generated textbook content (textbook_data.json, textbook_vocab.json) and third-party source files are gitignored — re-run Scripts/textbook/run_pipeline.sh locally to regenerate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
3.6 KiB
Swift
96 lines
3.6 KiB
Swift
import XCTest
|
||
|
||
/// Screenshot every chapter of the textbook — one top + one bottom frame each —
|
||
/// so you can visually audit parsing / rendering issues across all 30 chapters.
|
||
final class AllChaptersScreenshotTests: XCTestCase {
|
||
|
||
override func setUpWithError() throws {
|
||
continueAfterFailure = true
|
||
}
|
||
|
||
func testScreenshotEveryChapter() throws {
|
||
let app = XCUIApplication()
|
||
app.launchArguments += ["-onboardingComplete", "YES"]
|
||
app.launch()
|
||
|
||
let courseTab = app.tabBars.buttons["Course"]
|
||
XCTAssertTrue(courseTab.waitForExistence(timeout: 5))
|
||
courseTab.tap()
|
||
|
||
let textbookRow = app.buttons.containing(NSPredicate(
|
||
format: "label CONTAINS[c] 'Complete Spanish'"
|
||
)).firstMatch
|
||
XCTAssertTrue(textbookRow.waitForExistence(timeout: 5))
|
||
textbookRow.tap()
|
||
|
||
// NOTE: SwiftUI List preserves scroll position across navigation pushes,
|
||
// so visiting chapters in-order means the next one is already visible
|
||
// after we return from the previous one. No need to reset.
|
||
attach(app, name: "00-chapter-list-top")
|
||
|
||
for chapter in 1...30 {
|
||
guard let row = findChapterRow(app: app, chapter: chapter) else {
|
||
XCTFail("Chapter \(chapter) row not reachable")
|
||
continue
|
||
}
|
||
row.tap()
|
||
|
||
// Chapter body — wait until the chapter's title appears as a nav bar label
|
||
_ = app.navigationBars.firstMatch.waitForExistence(timeout: 3)
|
||
|
||
attach(app, name: String(format: "ch%02d-top", chapter))
|
||
// One big scroll to sample the bottom of the chapter
|
||
dragFullScreen(app, direction: .up)
|
||
dragFullScreen(app, direction: .up)
|
||
attach(app, name: String(format: "ch%02d-bottom", chapter))
|
||
|
||
tapNavBack(app)
|
||
// Small settle wait
|
||
_ = app.navigationBars.firstMatch.waitForExistence(timeout: 2)
|
||
}
|
||
}
|
||
|
||
// MARK: - Helpers
|
||
|
||
private enum DragDirection { case up, down }
|
||
|
||
private func dragFullScreen(_ app: XCUIApplication, direction: DragDirection) {
|
||
let top = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.12))
|
||
let bot = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.88))
|
||
switch direction {
|
||
case .up: bot.press(forDuration: 0.1, thenDragTo: top)
|
||
case .down: top.press(forDuration: 0.1, thenDragTo: bot)
|
||
}
|
||
}
|
||
|
||
private func findChapterRow(app: XCUIApplication, chapter: Int) -> XCUIElement? {
|
||
// Chapter row accessibility label: "<n>, <title>, ..." (SwiftUI composes
|
||
// label from inner Texts). Match by starting number.
|
||
let predicate = NSPredicate(format: "label BEGINSWITH %@", "\(chapter),")
|
||
let row = app.buttons.matching(predicate).firstMatch
|
||
|
||
if row.exists && row.isHittable { return row }
|
||
|
||
// Scroll down up to 8 times searching for the row — chapters visited
|
||
// in order, so usually 0–2 swipes suffice.
|
||
for _ in 0..<8 {
|
||
if row.exists && row.isHittable { return row }
|
||
dragFullScreen(app, direction: .up)
|
||
}
|
||
return row.exists ? row : nil
|
||
}
|
||
|
||
private func tapNavBack(_ app: XCUIApplication) {
|
||
let back = app.navigationBars.buttons.firstMatch
|
||
if back.exists && back.isHittable { back.tap() }
|
||
}
|
||
|
||
private func attach(_ app: XCUIApplication, name: String) {
|
||
let screenshot = app.screenshot()
|
||
let attachment = XCTAttachment(screenshot: screenshot)
|
||
attachment.name = name
|
||
attachment.lifetime = .keepAlways
|
||
add(attachment)
|
||
}
|
||
}
|