From 93894e4c257adf080e7c7d9a67bbb2d4aef5a00f Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 29 Jan 2026 09:33:43 -0500 Subject: [PATCH] feat: Adds pdf client tests, updates view controller snapshots that have changed. --- Package.swift | 13 + .../Middleware/DependenciesMiddleware.swift | 1 + Sources/App/configure.swift | 42 +- Sources/FileClient/Interface.swift | 29 +- Sources/PdfClient/Interface.swift | 14 +- Sources/ProjectClient/Interface.swift | 7 +- Sources/ProjectClient/Live.swift | 16 +- Tests/PdfClientTests/PdfClientTests.swift | 26 + .../__Snapshots__/PdfClientTests/html.1.html | 583 ++++++++++++++++++ .../ViewControllerTests.swift | 2 +- .../ViewControllerTests/login.1.html | 2 + .../ViewControllerTests/projectDetail.1.html | 2 + .../ViewControllerTests/projectDetail.2.html | 2 + .../ViewControllerTests/projectDetail.3.html | 2 + .../ViewControllerTests/projectDetail.4.html | 2 + .../ViewControllerTests/projectDetail.5.html | 2 + .../ViewControllerTests/projectDetail.6.html | 6 +- .../ViewControllerTests/projectIndex.1.html | 2 + .../ViewControllerTests/signup.1.html | 2 + .../ViewControllerTests/userProfile.1.html | 2 + 20 files changed, 694 insertions(+), 63 deletions(-) create mode 100644 Tests/PdfClientTests/PdfClientTests.swift create mode 100644 Tests/PdfClientTests/__Snapshots__/PdfClientTests/html.1.html diff --git a/Package.swift b/Package.swift index 625543d..45082d0 100644 --- a/Package.swift +++ b/Package.swift @@ -93,6 +93,7 @@ let package = Package( dependencies: [ .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"), + .product(name: "Vapor", package: "vapor"), ] ), .target( @@ -113,6 +114,18 @@ let package = Package( .product(name: "Elementary", package: "elementary"), ] ), + .testTarget( + name: "PdfClientTests", + dependencies: [ + .target(name: "HTMLSnapshotTesting"), + .target(name: "PdfClient"), + .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), + ] + // , + // resources: [ + // .copy("__Snapshots__") + // ] + ), .target( name: "ProjectClient", dependencies: [ diff --git a/Sources/App/Middleware/DependenciesMiddleware.swift b/Sources/App/Middleware/DependenciesMiddleware.swift index 4ef42a9..8a16843 100644 --- a/Sources/App/Middleware/DependenciesMiddleware.swift +++ b/Sources/App/Middleware/DependenciesMiddleware.swift @@ -36,6 +36,7 @@ struct DependenciesMiddleware: AsyncMiddleware { // $0.dateFormatter = .liveValue $0.viewController = viewController $0.pdfClient = .liveValue + $0.fileClient = .live(fileIO: request.fileio) } operation: { try await next.respond(to: request) } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 941a74c..6bc6232 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -114,43 +114,6 @@ extension SiteRoute { extension DuctSizes: Content {} -// FIX: Move -func handlePdf(_ projectID: Project.ID, on request: Request) async throws -> Response { - @Dependency(\.projectClient) var projectClient - return try await projectClient.generatePdf(projectID, request.fileio) - - // let html = try await projectClient.toHTML(projectID) - // let url = "/tmp/\(projectID)" - // try await request.fileio.writeFile(.init(string: html.render()), at: "\(url).html") - // - // let process = Process() - // let standardInput = Pipe() - // let standardOutput = Pipe() - // process.standardInput = standardInput - // process.standardOutput = standardOutput - // process.executableURL = URL(fileURLWithPath: "/bin/pandoc") - // process.arguments = [ - // "\(url).html", - // "--pdf-engine=weasyprint", - // "--from=html", - // "--css=Public/css/pdf.css", - // "-o", "\(url).pdf", - // ] - // try process.run() - // process.waitUntilExit() - // - // let response = try await request.fileio.asyncStreamFile(at: "\(url).pdf", mediaType: .pdf) { _ in - // // Remove files here. - // try FileManager.default.removeItem(atPath: "\(url).pdf") - // try FileManager.default.removeItem(atPath: "\(url).html") - // } - // response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream") - // response.headers.replaceOrAdd( - // name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf" - // ) - // return response -} - @Sendable private func siteHandler( request: Request, @@ -165,9 +128,10 @@ private func siteHandler( return try await apiController.respond(route, request: request) case .health: return HTTPStatus.ok - // FIX: Move + // Generating a pdf return's a `Response` instead of `HTML` like other views, so we + // need to handle it seperately. case .view(.project(.detail(let projectID, .pdf))): - return try await handlePdf(projectID, on: request) + return try await projectClient.generatePdf(projectID) case .view(let route): return try await viewController.respond(route: route, request: request) } diff --git a/Sources/FileClient/Interface.swift b/Sources/FileClient/Interface.swift index f29ff42..359f8ea 100644 --- a/Sources/FileClient/Interface.swift +++ b/Sources/FileClient/Interface.swift @@ -1,6 +1,7 @@ import Dependencies import DependenciesMacros import Foundation +import Vapor extension DependencyValues { public var fileClient: FileClient { @@ -11,19 +12,29 @@ extension DependencyValues { @DependencyClient public struct FileClient: Sendable { + public typealias OnCompleteHandler = @Sendable () async throws -> Void + public var writeFile: @Sendable (String, String) async throws -> Void public var removeFile: @Sendable (String) async throws -> Void + public var streamFile: @Sendable (String, @escaping OnCompleteHandler) async throws -> Response } -extension FileClient: DependencyKey { +extension FileClient: TestDependencyKey { public static let testValue = Self() - public static let liveValue = Self( - writeFile: { contents, path in - try contents.write(to: URL(fileURLWithPath: path), atomically: true, encoding: .utf8) - }, - removeFile: { path in - try FileManager.default.removeItem(atPath: path) - } - ) + public static func live(fileIO: FileIO) -> Self { + .init( + writeFile: { contents, path in + try await fileIO.writeFile(ByteBuffer(string: contents), at: path) + }, + removeFile: { path in + try FileManager.default.removeItem(atPath: path) + }, + streamFile: { path, onComplete in + try await fileIO.asyncStreamFile(at: path) { _ in + try await onComplete() + } + } + ) + } } diff --git a/Sources/PdfClient/Interface.swift b/Sources/PdfClient/Interface.swift index 008e881..6aa0b05 100644 --- a/Sources/PdfClient/Interface.swift +++ b/Sources/PdfClient/Interface.swift @@ -8,6 +8,8 @@ import ManualDCore extension DependencyValues { + /// Access the pdf client dependency that can be used to generate pdf's for + /// a project. public var pdfClient: PdfClient { get { self[PdfClient.self] } set { self[PdfClient.self] = newValue } @@ -16,9 +18,19 @@ extension DependencyValues { @DependencyClient public struct PdfClient: Sendable { + /// Generate the html used to convert to pdf for a project. public var html: @Sendable (Request) async throws -> (any HTML & Sendable) + + /// Converts the generated html to a pdf. + /// + /// **NOTE:** This is generally not used directly, instead use the overload that accepts a request, + /// which generates the html and does the conversion all in one step. public var generatePdf: @Sendable (Project.ID, any HTML & Sendable) async throws -> Response + /// Generate a pdf for the given project request. + /// + /// - Parameters: + /// - request: The project data used to generate the pdf. public func generatePdf(request: Request) async throws -> Response { let html = try await self.html(request) return try await self.generatePdf(request.project.id, html) @@ -64,7 +76,7 @@ extension PdfClient: DependencyKey { } extension PdfClient { - + /// Container for the data required to generate a pdf for a given project. public struct Request: Codable, Equatable, Sendable { public let project: Project diff --git a/Sources/ProjectClient/Interface.swift b/Sources/ProjectClient/Interface.swift index 5ba9cb9..47fd81a 100644 --- a/Sources/ProjectClient/Interface.swift +++ b/Sources/ProjectClient/Interface.swift @@ -28,12 +28,7 @@ public struct ProjectClient: Sendable { @Sendable (User.ID, Project.Create) async throws -> CreateProjectResponse public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse - - // FIX: Name to something to do with generating a pdf, just experimenting now. - // public var toMarkdown: @Sendable (Project.ID) async throws -> String - // public var toHTML: @Sendable (Project.ID) async throws -> (any HTML & Sendable) - - public var generatePdf: @Sendable (Project.ID, FileIO) async throws -> Response + public var generatePdf: @Sendable (Project.ID) async throws -> Response } extension ProjectClient: TestDependencyKey { diff --git a/Sources/ProjectClient/Live.swift b/Sources/ProjectClient/Live.swift index fbb0e58..9d3b29c 100644 --- a/Sources/ProjectClient/Live.swift +++ b/Sources/ProjectClient/Live.swift @@ -37,14 +37,18 @@ extension ProjectClient: DependencyKey { frictionRate: { projectID in try await manualD.frictionRate(projectID: projectID) }, - generatePdf: { projectID, fileIO in + generatePdf: { projectID in let pdfResponse = try await pdfClient.generatePdf( - request: database.makePdfRequest(projectID)) + request: database.makePdfRequest(projectID) + ) - let response = try await fileIO.asyncStreamFile(at: pdfResponse.pdfPath) { _ in - try await fileClient.removeFile(pdfResponse.htmlPath) - try await fileClient.removeFile(pdfResponse.pdfPath) - } + let response = try await fileClient.streamFile( + pdfResponse.pdfPath, + { + try await fileClient.removeFile(pdfResponse.htmlPath) + try await fileClient.removeFile(pdfResponse.pdfPath) + } + ) response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream") response.headers.replaceOrAdd( diff --git a/Tests/PdfClientTests/PdfClientTests.swift b/Tests/PdfClientTests/PdfClientTests.swift new file mode 100644 index 0000000..7fcd0e0 --- /dev/null +++ b/Tests/PdfClientTests/PdfClientTests.swift @@ -0,0 +1,26 @@ +import Dependencies +import Foundation +import HTMLSnapshotTesting +import PdfClient +import SnapshotTesting +import Testing + +@Suite(.snapshots(record: .missing)) +struct PdfClientTests { + + @Test + func html() async throws { + + try await withDependencies { + $0.pdfClient = .liveValue + $0.uuid = .incrementing + $0.date.now = Date(timeIntervalSince1970: 1_234_567_890) + } operation: { + @Dependency(\.pdfClient) var pdfClient + + let html = try await pdfClient.html(.mock()) + assertSnapshot(of: html, as: .html) + } + + } +} diff --git a/Tests/PdfClientTests/__Snapshots__/PdfClientTests/html.1.html b/Tests/PdfClientTests/__Snapshots__/PdfClientTests/html.1.html new file mode 100644 index 0000000..5a18633 --- /dev/null +++ b/Tests/PdfClientTests/__Snapshots__/PdfClientTests/html.1.html @@ -0,0 +1,583 @@ + + + + Duct Calc + + + +
+

Project

+
+ + + + + + + + + + + +
NameTesty McTestface
Address +

+ 1234 Sesame Street +
+ Monroe, OH 55555 +

+
+
+
+
+
+

Equipment

+

Friction Rate

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
EquipmentValue
Static Pressure0.5
Heating CFM900
Cooling CFM1,000
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Friction RateValue
evaporator-coil0.2
filter0.1
supply-outlet0.03
return-grille0.03
balancing-damper0.03
+
+
+
+
+
+

Duct Sizes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDsn CFMRound SizeVelocityFinal SizeFlex SizeHeightWidth
Bed-192748988
Entry88748988
Entry88748988
Family Room92748988
Family Room92748988
Family Room92748988
Kitchen95748988
Kitchen95748988
Living Room127748988
Living Room127748988
Master87748988
Master87748988
+
+
+

Supply Trunk / Run Outs

+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameDsn CFMRound SizeVelocityFinal SizeFlex SizeHeightWidth
1,000189872020
+
+
+

Return Trunk / Run Outs

+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameDsn CFMRound SizeVelocityFinal SizeFlex SizeHeightWidth
1,000189872020
+
+
+

Total Equivalent Lengths

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeStraight LengthsGroupsTotal
Supply - 1supply +
    +
  • 10
  • +
  • 25
  • +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameLengthQuantityTotal
1-a20120
2-b30130
3-a10110
12-a10110
+
105
Return - 1return +
    +
  • 10
  • +
  • 20
  • +
  • 5
  • +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameLengthQuantityTotal
5-a10110
6-a15115
7-a20120
+
80
+
+
+

Register Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameHeating BTUCooling BTUHeating CFMCooling CFMDesign CFM
Bed-13,9132,472839292
Entry4,1421,458885488
Entry4,1421,458885488
Family Room3,2622,482699292
Family Room3,2622,482699292
Family Room3,2622,482699292
Kitchen2,2592,548489595
Kitchen2,2592,548489595
Living Room3,7763,41480127127
Living Room3,7763,41480127127
Master4,1011,038873987
Master4,1011,038873987
+
+
+

Room Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameHeating BTUCooling Total BTUCooling Sensible BTURegister Count
Bed-13,9132,4722,0521
Entry8,2842,9162,4202
Family Room9,7857,4466,1803
Kitchen4,5185,0964,2302
Living Room7,5536,8295,6682
Master8,2022,0761,7232
Totals42,25526,83522,273
+
+
+ + \ No newline at end of file diff --git a/Tests/ViewControllerTests/ViewControllerTests.swift b/Tests/ViewControllerTests/ViewControllerTests.swift index 6626c23..bc29891 100644 --- a/Tests/ViewControllerTests/ViewControllerTests.swift +++ b/Tests/ViewControllerTests/ViewControllerTests.swift @@ -11,7 +11,7 @@ import SnapshotTesting import Testing import ViewController -@Suite(.snapshots(record: .missing)) +@Suite(.snapshots(record: .failed)) struct ViewControllerTests { @Test diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/login.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/login.1.html index c044deb..a6dc2a8 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/login.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/login.1.html @@ -16,8 +16,10 @@ + + diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html index 4a98913..ca19d9c 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html @@ -16,8 +16,10 @@ + + diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.2.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.2.html index e0d851c..12a954d 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.2.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.2.html @@ -16,8 +16,10 @@ + + diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.3.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.3.html index 7ba7bc4..0513085 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.3.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.3.html @@ -16,8 +16,10 @@ + + diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.4.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.4.html index 694d334..443518e 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.4.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.4.html @@ -16,8 +16,10 @@ + + diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.5.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.5.html index e0ce9c0..4bf598b 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.5.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.5.html @@ -16,8 +16,10 @@ + + diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.6.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.6.html index a579ecf..0013fdf 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.6.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.6.html @@ -16,8 +16,10 @@ + + @@ -55,7 +57,9 @@ p-6 w-full">

Must complete all the previous sections to display duct sizing calculations.

- PDF +
+ +
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectIndex.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectIndex.1.html index 8f6b58a..e1f8f9f 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectIndex.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectIndex.1.html @@ -16,8 +16,10 @@ + + diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/signup.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/signup.1.html index c044deb..a6dc2a8 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/signup.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/signup.1.html @@ -16,8 +16,10 @@ + + diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userProfile.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userProfile.1.html index 6a7df06..62a38d4 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userProfile.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/userProfile.1.html @@ -16,8 +16,10 @@ + +