| @@ -1,7 +1,14 @@ | |||||
| { | { | ||||
| "permissions": { | "permissions": { | ||||
| "allow": [ | "allow": [ | ||||
| "Bash(powershell -Command \"Get-Content ''d:\\\\Development\\\\Tracking_Kits\\\\ImportService\\\\TrackingDataImport.vbs'' -Encoding Unicode | Out-String\")" | |||||
| "Bash(powershell -Command \"Get-Content ''d:\\\\Development\\\\Tracking_Kits\\\\ImportService\\\\TrackingDataImport.vbs'' -Encoding Unicode | Out-String\")", | |||||
| "Bash(git -C \"d:/Development/Tracking_Kits\" rev-parse HEAD)", | |||||
| "Bash(cscript //NoLogo Tests/TrackingDataImport_TestHarness.vbs)", | |||||
| "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix_harness.ps1\")", | |||||
| "Bash(powershell -Command \":*)", | |||||
| "Bash(del \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix_harness.ps1\")", | |||||
| "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix2.ps1\")", | |||||
| "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix3.ps1\")" | |||||
| ] | ] | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,6 +1,7 @@ | |||||
| <% Option Explicit %> | <% Option Explicit %> | ||||
| <!--#include file="../../include_all.asp"--> | <!--#include file="../../include_all.asp"--> | ||||
| <!--#include file="../../ViewModels/KitViewModels.asp"--> | <!--#include file="../../ViewModels/KitViewModels.asp"--> | ||||
| <!--#include file="../../DomainModels/PurpleEnvelopeReportHelper.asp"--> | |||||
| <% | <% | ||||
| Class KitController | Class KitController | ||||
| Public Model | Public Model | ||||
| @@ -40,9 +41,7 @@ Class KitController | |||||
| dim ID : ID = Request.Form("Id") | dim ID : ID = Request.Form("Id") | ||||
| dim model : set model = KitRepository.FindByID(ID) | dim model : set model = KitRepository.FindByID(ID) | ||||
| set model = Automapper.AutoMap(Request.Form, model) | set model = Automapper.AutoMap(Request.Form, model) | ||||
| if Request.Form("InBoundTracking") = "on" Then | |||||
| model.InboundSTID = SettingsRepository.FindByName("Inbound STID") | |||||
| end if | |||||
| model.InboundSTID = Null | |||||
| model.Status = "Ready to Assign Labels" | model.Status = "Ready to Assign Labels" | ||||
| @@ -99,10 +98,10 @@ Class KitController | |||||
| set Model.Kit = KitRepository.SwitchBoardPurpleEnvelopeEditFindById(id) | set Model.Kit = KitRepository.SwitchBoardPurpleEnvelopeEditFindById(id) | ||||
| set Model.StidDropDown = SettingsRepository.GetStidDropDownRS() | set Model.StidDropDown = SettingsRepository.GetStidDropDownRS() | ||||
| set Model.ColorsDropDown = ColorsRepository.GetColorsDropDownRS() | set Model.ColorsDropDown = ColorsRepository.GetColorsDropDownRS() | ||||
| set Model.Precincts = SortPrecinctColorRows(InkjetRecordsRepository.GetDistinctPrecinctsByKitId(id)) | |||||
| set Model.PrecinctBallotRanges = SortPrecinctBallotRangeRows(InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(id)) | |||||
| set Model.Precincts = PurpleEnvelopeReportHelper().SortPrecinctColorRows(InkjetRecordsRepository.GetDistinctPrecinctsByKitId(id)) | |||||
| set Model.PrecinctBallotRanges = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(id)) | |||||
| On Error Resume Next | On Error Resume Next | ||||
| Model.PurpleEnvelopeElectionLabel = FormatPurpleEnvelopeElectionLabel(SettingsRepository.FindByName("Electiondate")) | |||||
| Model.PurpleEnvelopeElectionLabel = PurpleEnvelopeReportHelper().FormatElectionLabel(SettingsRepository.FindByName("Electiondate")) | |||||
| If Err.Number <> 0 Then | If Err.Number <> 0 Then | ||||
| Model.PurpleEnvelopeElectionLabel = "" | Model.PurpleEnvelopeElectionLabel = "" | ||||
| Err.Clear | Err.Clear | ||||
| @@ -114,158 +113,6 @@ Class KitController | |||||
| %> <!--#include file="../../Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp"--> <% | %> <!--#include file="../../Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp"--> <% | ||||
| End Sub | End Sub | ||||
| Private Function FormatPurpleEnvelopeElectionLabel(ByVal rawValue) | |||||
| FormatPurpleEnvelopeElectionLabel = Trim(rawValue & "") | |||||
| If Len(FormatPurpleEnvelopeElectionLabel) = 0 Then Exit Function | |||||
| On Error Resume Next | |||||
| dim parsedDate : parsedDate = CDate(rawValue) | |||||
| If Err.Number = 0 Then | |||||
| FormatPurpleEnvelopeElectionLabel = MonthName(Month(parsedDate), True) & "-" & CStr(Year(parsedDate)) | |||||
| Else | |||||
| Err.Clear | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| End Function | |||||
| Private Function SortPrecinctColorRows(ByVal rs) | |||||
| dim list : set list = new LinkedList_Class | |||||
| If rs.EOF Then | |||||
| set SortPrecinctColorRows = list | |||||
| Exit Function | |||||
| End If | |||||
| dim items() | |||||
| dim itemCount : itemCount = -1 | |||||
| Do Until rs.EOF | |||||
| itemCount = itemCount + 1 | |||||
| ReDim Preserve items(itemCount) | |||||
| dim row : set row = new PrecinctColorRow_ViewModel_Class | |||||
| row.PRECINCT = rs("PRECINCT") | |||||
| If IsNull(rs("ColorId")) Then | |||||
| row.ColorId = "" | |||||
| Else | |||||
| row.ColorId = rs("ColorId") | |||||
| End If | |||||
| Set items(itemCount) = row | |||||
| rs.MoveNext | |||||
| Loop | |||||
| SortPrecinctItems items | |||||
| dim i | |||||
| For i = 0 To UBound(items) | |||||
| list.Push items(i) | |||||
| Next | |||||
| set SortPrecinctColorRows = list | |||||
| End Function | |||||
| Private Function SortPrecinctBallotRangeRows(ByVal rs) | |||||
| dim list : set list = new LinkedList_Class | |||||
| If rs.EOF Then | |||||
| set SortPrecinctBallotRangeRows = list | |||||
| Exit Function | |||||
| End If | |||||
| dim items() | |||||
| dim itemCount : itemCount = -1 | |||||
| Do Until rs.EOF | |||||
| itemCount = itemCount + 1 | |||||
| ReDim Preserve items(itemCount) | |||||
| dim row : set row = new PrecinctBallotRangeRow_ViewModel_Class | |||||
| row.PRECINCT = rs("PRECINCT") | |||||
| row.LowBallotNumber = rs("LowBallotNumber") | |||||
| row.HighBallotNumber = rs("HighBallotNumber") | |||||
| Set items(itemCount) = row | |||||
| rs.MoveNext | |||||
| Loop | |||||
| SortPrecinctItems items | |||||
| dim i | |||||
| For i = 0 To UBound(items) | |||||
| list.Push items(i) | |||||
| Next | |||||
| set SortPrecinctBallotRangeRows = list | |||||
| End Function | |||||
| Private Sub SortPrecinctItems(ByRef items) | |||||
| If Not IsArray(items) Then Exit Sub | |||||
| dim i, j | |||||
| For i = 0 To UBound(items) - 1 | |||||
| For j = i + 1 To UBound(items) | |||||
| If PrecinctSortsBefore(items(j).PRECINCT, items(i).PRECINCT) Then | |||||
| dim temp : set temp = items(i) | |||||
| Set items(i) = items(j) | |||||
| Set items(j) = temp | |||||
| End If | |||||
| Next | |||||
| Next | |||||
| End Sub | |||||
| Private Function PrecinctSortsBefore(ByVal leftPrecinct, ByVal rightPrecinct) | |||||
| dim leftType, leftNumber, leftSuffix, leftNormalized | |||||
| dim rightType, rightNumber, rightSuffix, rightNormalized | |||||
| ParsePrecinctSortParts leftPrecinct, leftType, leftNumber, leftSuffix, leftNormalized | |||||
| ParsePrecinctSortParts rightPrecinct, rightType, rightNumber, rightSuffix, rightNormalized | |||||
| If leftType <> rightType Then | |||||
| PrecinctSortsBefore = (leftType < rightType) | |||||
| Exit Function | |||||
| End If | |||||
| If leftType = 0 Then | |||||
| If leftNumber <> rightNumber Then | |||||
| PrecinctSortsBefore = (leftNumber < rightNumber) | |||||
| Exit Function | |||||
| End If | |||||
| If leftSuffix <> rightSuffix Then | |||||
| PrecinctSortsBefore = (leftSuffix < rightSuffix) | |||||
| Exit Function | |||||
| End If | |||||
| End If | |||||
| If leftNormalized <> rightNormalized Then | |||||
| PrecinctSortsBefore = (leftNormalized < rightNormalized) | |||||
| Else | |||||
| PrecinctSortsBefore = (UCase(Trim(leftPrecinct & "")) < UCase(Trim(rightPrecinct & ""))) | |||||
| End If | |||||
| End Function | |||||
| Private Sub ParsePrecinctSortParts(ByVal precinct, ByRef precinctType, ByRef numericPart, ByRef suffixPart, ByRef normalizedText) | |||||
| dim rawPrecinct : rawPrecinct = Trim(precinct & "") | |||||
| dim leadingDigits : leadingDigits = "" | |||||
| dim i, currentChar | |||||
| For i = 1 To Len(rawPrecinct) | |||||
| currentChar = Mid(rawPrecinct, i, 1) | |||||
| If currentChar >= "0" And currentChar <= "9" Then | |||||
| leadingDigits = leadingDigits & currentChar | |||||
| Else | |||||
| Exit For | |||||
| End If | |||||
| Next | |||||
| If Len(leadingDigits) > 0 Then | |||||
| precinctType = 0 | |||||
| numericPart = CLng(leadingDigits) | |||||
| suffixPart = UCase(Trim(Mid(rawPrecinct, Len(leadingDigits) + 1))) | |||||
| normalizedText = CStr(numericPart) & "|" & suffixPart | |||||
| Else | |||||
| precinctType = 1 | |||||
| numericPart = 0 | |||||
| suffixPart = UCase(rawPrecinct) | |||||
| normalizedText = suffixPart | |||||
| End If | |||||
| End Sub | |||||
| Public Sub Index | Public Sub Index | ||||
| dim page_size : page_size = 10 | dim page_size : page_size = 10 | ||||
| @@ -0,0 +1,164 @@ | |||||
| <% | |||||
| Class PurpleEnvelopeReportHelper_Class | |||||
| Public Function FormatElectionLabel(ByVal rawValue) | |||||
| FormatElectionLabel = Trim(rawValue & "") | |||||
| If Len(FormatElectionLabel) = 0 Then Exit Function | |||||
| On Error Resume Next | |||||
| dim parsedDate : parsedDate = CDate(rawValue) | |||||
| If Err.Number = 0 Then | |||||
| FormatElectionLabel = MonthName(Month(parsedDate), True) & "-" & CStr(Year(parsedDate)) | |||||
| Else | |||||
| Err.Clear | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| End Function | |||||
| Public Function SortPrecinctColorRows(ByVal rs) | |||||
| dim list : set list = new LinkedList_Class | |||||
| If rs.EOF Then | |||||
| set SortPrecinctColorRows = list | |||||
| Exit Function | |||||
| End If | |||||
| dim items() | |||||
| dim itemCount : itemCount = -1 | |||||
| Do Until rs.EOF | |||||
| itemCount = itemCount + 1 | |||||
| ReDim Preserve items(itemCount) | |||||
| dim row : set row = new PrecinctColorRow_ViewModel_Class | |||||
| row.PRECINCT = rs("PRECINCT") | |||||
| If IsNull(rs("ColorId")) Then | |||||
| row.ColorId = "" | |||||
| Else | |||||
| row.ColorId = rs("ColorId") | |||||
| End If | |||||
| Set items(itemCount) = row | |||||
| rs.MoveNext | |||||
| Loop | |||||
| SortPrecinctItems items | |||||
| dim i | |||||
| For i = 0 To UBound(items) | |||||
| list.Push items(i) | |||||
| Next | |||||
| set SortPrecinctColorRows = list | |||||
| End Function | |||||
| Public Function SortPrecinctBallotRangeRows(ByVal rs) | |||||
| dim list : set list = new LinkedList_Class | |||||
| If rs.EOF Then | |||||
| set SortPrecinctBallotRangeRows = list | |||||
| Exit Function | |||||
| End If | |||||
| dim items() | |||||
| dim itemCount : itemCount = -1 | |||||
| Do Until rs.EOF | |||||
| itemCount = itemCount + 1 | |||||
| ReDim Preserve items(itemCount) | |||||
| dim row : set row = new PrecinctBallotRangeRow_ViewModel_Class | |||||
| row.PRECINCT = rs("PRECINCT") | |||||
| row.LowBallotNumber = rs("LowBallotNumber") | |||||
| row.HighBallotNumber = rs("HighBallotNumber") | |||||
| Set items(itemCount) = row | |||||
| rs.MoveNext | |||||
| Loop | |||||
| SortPrecinctItems items | |||||
| dim i | |||||
| For i = 0 To UBound(items) | |||||
| list.Push items(i) | |||||
| Next | |||||
| set SortPrecinctBallotRangeRows = list | |||||
| End Function | |||||
| Private Sub SortPrecinctItems(ByRef items) | |||||
| If Not IsArray(items) Then Exit Sub | |||||
| dim i, j | |||||
| For i = 0 To UBound(items) - 1 | |||||
| For j = i + 1 To UBound(items) | |||||
| If PrecinctSortsBefore(items(j).PRECINCT, items(i).PRECINCT) Then | |||||
| dim temp : set temp = items(i) | |||||
| Set items(i) = items(j) | |||||
| Set items(j) = temp | |||||
| End If | |||||
| Next | |||||
| Next | |||||
| End Sub | |||||
| Private Function PrecinctSortsBefore(ByVal leftPrecinct, ByVal rightPrecinct) | |||||
| dim leftType, leftNumber, leftSuffix, leftNormalized | |||||
| dim rightType, rightNumber, rightSuffix, rightNormalized | |||||
| ParsePrecinctSortParts leftPrecinct, leftType, leftNumber, leftSuffix, leftNormalized | |||||
| ParsePrecinctSortParts rightPrecinct, rightType, rightNumber, rightSuffix, rightNormalized | |||||
| If leftType <> rightType Then | |||||
| PrecinctSortsBefore = (leftType < rightType) | |||||
| Exit Function | |||||
| End If | |||||
| If leftType = 0 Then | |||||
| If leftNumber <> rightNumber Then | |||||
| PrecinctSortsBefore = (leftNumber < rightNumber) | |||||
| Exit Function | |||||
| End If | |||||
| If leftSuffix <> rightSuffix Then | |||||
| PrecinctSortsBefore = (leftSuffix < rightSuffix) | |||||
| Exit Function | |||||
| End If | |||||
| End If | |||||
| If leftNormalized <> rightNormalized Then | |||||
| PrecinctSortsBefore = (leftNormalized < rightNormalized) | |||||
| Else | |||||
| PrecinctSortsBefore = (UCase(Trim(leftPrecinct & "")) < UCase(Trim(rightPrecinct & ""))) | |||||
| End If | |||||
| End Function | |||||
| Private Sub ParsePrecinctSortParts(ByVal precinct, ByRef precinctType, ByRef numericPart, ByRef suffixPart, ByRef normalizedText) | |||||
| dim rawPrecinct : rawPrecinct = Trim(precinct & "") | |||||
| dim leadingDigits : leadingDigits = "" | |||||
| dim i, currentChar | |||||
| For i = 1 To Len(rawPrecinct) | |||||
| currentChar = Mid(rawPrecinct, i, 1) | |||||
| If currentChar >= "0" And currentChar <= "9" Then | |||||
| leadingDigits = leadingDigits & currentChar | |||||
| Else | |||||
| Exit For | |||||
| End If | |||||
| Next | |||||
| If Len(leadingDigits) > 0 Then | |||||
| precinctType = 0 | |||||
| numericPart = CLng(leadingDigits) | |||||
| suffixPart = UCase(Trim(Mid(rawPrecinct, Len(leadingDigits) + 1))) | |||||
| normalizedText = CStr(numericPart) & "|" & suffixPart | |||||
| Else | |||||
| precinctType = 1 | |||||
| numericPart = 0 | |||||
| suffixPart = UCase(rawPrecinct) | |||||
| normalizedText = suffixPart | |||||
| End If | |||||
| End Sub | |||||
| End Class | |||||
| dim PurpleEnvelopeReportHelper__Singleton | |||||
| Function PurpleEnvelopeReportHelper() | |||||
| If IsEmpty(PurpleEnvelopeReportHelper__Singleton) Then | |||||
| set PurpleEnvelopeReportHelper__Singleton = new PurpleEnvelopeReportHelper_Class | |||||
| End If | |||||
| set PurpleEnvelopeReportHelper = PurpleEnvelopeReportHelper__Singleton | |||||
| End Function | |||||
| %> | |||||
| @@ -105,8 +105,6 @@ | |||||
| <%= HTML.Hidden("Id", Model.Kit.ID) %> | <%= HTML.Hidden("Id", Model.Kit.ID) %> | ||||
| <%= HTML.DropDownListExt("OutboundSTID","hmm",Model.StidDropDown,"STID","OPTION",Array("Class","form-select")) %> | <%= HTML.DropDownListExt("OutboundSTID","hmm",Model.StidDropDown,"STID","OPTION",Array("Class","form-select")) %> | ||||
| <p></p> | <p></p> | ||||
| <%= HTML.Checkbox("InBoundTracking",0) %><strong>Inbound Tracking</strong> | |||||
| <p></p> | |||||
| <p><%= HTML.Button("submit", "<i class='glyphicon glyphicon-ok'></i> Save", "btn-primary") %></p> | <p><%= HTML.Button("submit", "<i class='glyphicon glyphicon-ok'></i> Save", "btn-primary") %></p> | ||||
| </form> | </form> | ||||
| <p></p> | <p></p> | ||||
| @@ -18,12 +18,44 @@ select case dev | |||||
| ExportDirectory ="\\kci-syn-cl01\PC Transfer\TrackingDataExport\" | ExportDirectory ="\\kci-syn-cl01\PC Transfer\TrackingDataExport\" | ||||
| end select | end select | ||||
| Function LoadChilkatSerial() | |||||
| dim fallbackSerial : fallbackSerial = "KENTCM.CB1022025_RGzBPM5J655e" | |||||
| LoadChilkatSerial = fallbackSerial | |||||
| On Error Resume Next | |||||
| dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject") | |||||
| dim serialPath : serialPath = Request.ServerVariables("APPL_PHYSICAL_PATH") & "Tests\chillkat_serial" | |||||
| If Err.Number = 0 Then | |||||
| If fso.FileExists(serialPath) Then | |||||
| dim serialFile : set serialFile = fso.OpenTextFile(serialPath, 1, False) | |||||
| If Err.Number = 0 Then | |||||
| dim fileSerial : fileSerial = Trim(serialFile.ReadAll()) | |||||
| serialFile.Close | |||||
| set serialFile = Nothing | |||||
| If Len(fileSerial) > 0 Then | |||||
| LoadChilkatSerial = fileSerial | |||||
| End If | |||||
| Else | |||||
| Err.Clear | |||||
| End If | |||||
| End If | |||||
| Else | |||||
| Err.Clear | |||||
| End If | |||||
| set fso = Nothing | |||||
| On Error GoTo 0 | |||||
| End Function | |||||
| '======================================================================================================================= | '======================================================================================================================= | ||||
| ' Set Global Variables Here | ' Set Global Variables Here | ||||
| '======================================================================================================================= | '======================================================================================================================= | ||||
| dim glob:set glob = Server.CreateObject("Chilkat_9_5_0.Global") | dim glob:set glob = Server.CreateObject("Chilkat_9_5_0.Global") | ||||
| dim success:success = glob.UnlockBundle("KENTCM.CB1022025_RGzBPM5J655e") | |||||
| dim success:success = glob.UnlockBundle(LoadChilkatSerial()) | |||||
| If (success <> 1) Then | If (success <> 1) Then | ||||
| put(glob.LastErrorText) | put(glob.LastErrorText) | ||||
| End If | End If | ||||
| %> | |||||
| %> | |||||
| @@ -0,0 +1,386 @@ | |||||
| <!--#include file="../App/ViewModels/KitViewModels.asp"--> | |||||
| <!--#include file="../App/DAL/lib.DAL.asp"--> | |||||
| <!--#include file="../App/DomainModels/JurisdictionRepository.asp"--> | |||||
| <!--#include file="../App/DomainModels/KitRepository.asp"--> | |||||
| <!--#include file="../App/DomainModels/InkjetRecordsRepository.asp"--> | |||||
| <!--#include file="../App/DomainModels/PurpleEnvelopeReportHelper.asp"--> | |||||
| <% | |||||
| Class PurpleEnvelopeReport_Tests | |||||
| Private m_tempDbPath | |||||
| Public Sub Setup | |||||
| m_tempDbPath = CreateDisposableDatabaseCopy() | |||||
| UseDisposableDatabase m_tempDbPath | |||||
| End Sub | |||||
| Public Sub Teardown | |||||
| ResetDisposableDatabase | |||||
| End Sub | |||||
| Public Function TestCaseNames | |||||
| TestCaseNames = Array( _ | |||||
| "Test_FormatElectionLabel_Returns_Mon_YYYY_For_Valid_Date", _ | |||||
| "Test_FormatElectionLabel_Returns_Trimmed_Raw_Value_For_Invalid_Date", _ | |||||
| "Test_SortPrecinctColorRows_Handles_Zero_Padded_And_Lettered_Precincts", _ | |||||
| "Test_SortPrecinctBallotRangeRows_Preserves_Ranges_While_Sorting", _ | |||||
| "Test_GetPrecinctBallotRangesByKitId_Uses_Seeded_Data", _ | |||||
| "Test_UpdateColorForKit_Updates_All_Seeded_Rows_For_The_Target_Kit", _ | |||||
| "Test_UpdateColorForPrecinct_Updates_Only_The_Targeted_Precinct", _ | |||||
| "Test_SwitchBoardPurpleEnvelopeEditFindById_Returns_Seeded_Header_Data", _ | |||||
| "Test_KitController_Post_Actions_Still_Delegate_To_Color_Update_Repositories", _ | |||||
| "Test_Report_View_Keeps_Print_Only_CSS_Contract", _ | |||||
| "Test_Report_View_Keeps_No_Data_Row_And_Page_Spacer" _ | |||||
| ) | |||||
| End Function | |||||
| Private Sub Destroy(ByRef o) | |||||
| On Error Resume Next | |||||
| o.Close | |||||
| On Error GoTo 0 | |||||
| Set o = Nothing | |||||
| End Sub | |||||
| Private Function CreatePrecinctColorRecordset(ByVal rows) | |||||
| dim rs : set rs = Server.CreateObject("ADODB.Recordset") | |||||
| With rs.Fields | |||||
| .Append "PRECINCT", adVarChar, 50 | |||||
| .Append "ColorId", adVarChar, 50 | |||||
| End With | |||||
| rs.Open | |||||
| dim i | |||||
| For i = 0 To UBound(rows) | |||||
| rs.AddNew | |||||
| rs("PRECINCT") = rows(i)(0) | |||||
| If IsNull(rows(i)(1)) Then | |||||
| rs("ColorId") = Null | |||||
| Else | |||||
| rs("ColorId") = rows(i)(1) | |||||
| End If | |||||
| rs.Update | |||||
| Next | |||||
| rs.MoveFirst | |||||
| set CreatePrecinctColorRecordset = rs | |||||
| End Function | |||||
| Private Function CreateBallotRangeRecordset(ByVal rows) | |||||
| dim rs : set rs = Server.CreateObject("ADODB.Recordset") | |||||
| With rs.Fields | |||||
| .Append "PRECINCT", adVarChar, 50 | |||||
| .Append "LowBallotNumber", adInteger | |||||
| .Append "HighBallotNumber", adInteger | |||||
| End With | |||||
| rs.Open | |||||
| dim i | |||||
| For i = 0 To UBound(rows) | |||||
| rs.AddNew | |||||
| rs("PRECINCT") = rows(i)(0) | |||||
| rs("LowBallotNumber") = rows(i)(1) | |||||
| rs("HighBallotNumber") = rows(i)(2) | |||||
| rs.Update | |||||
| Next | |||||
| rs.MoveFirst | |||||
| set CreateBallotRangeRecordset = rs | |||||
| End Function | |||||
| Private Function PrecinctListToCsv(ByVal list) | |||||
| dim values() | |||||
| dim idx : idx = -1 | |||||
| dim it : set it = list.Iterator() | |||||
| dim row | |||||
| Do While it.HasNext | |||||
| set row = it.GetNext() | |||||
| idx = idx + 1 | |||||
| ReDim Preserve values(idx) | |||||
| values(idx) = row.PRECINCT | |||||
| Loop | |||||
| If idx = -1 Then | |||||
| PrecinctListToCsv = "" | |||||
| Else | |||||
| PrecinctListToCsv = Join(values, ",") | |||||
| End If | |||||
| End Function | |||||
| Private Function ReadAllText(ByVal relativePath) | |||||
| dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject") | |||||
| dim fileHandle : set fileHandle = fso.OpenTextFile(Server.MapPath(relativePath), 1, False) | |||||
| ReadAllText = fileHandle.ReadAll() | |||||
| fileHandle.Close | |||||
| Set fileHandle = Nothing | |||||
| Set fso = Nothing | |||||
| End Function | |||||
| Private Function CreateDisposableDatabaseCopy() | |||||
| dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject") | |||||
| dim tempFolderPath : tempFolderPath = Server.MapPath("Temp") | |||||
| If Not fso.FolderExists(tempFolderPath) Then | |||||
| fso.CreateFolder tempFolderPath | |||||
| End If | |||||
| dim sourcePath : sourcePath = Server.MapPath("../Data/webdata - Copy.mdb") | |||||
| dim guidValue : guidValue = Mid(CreateObject("Scriptlet.TypeLib").Guid, 2, 36) | |||||
| guidValue = Replace(guidValue, "-", "") | |||||
| CreateDisposableDatabaseCopy = tempFolderPath & "\purple-envelope-tests-" & guidValue & ".mdb" | |||||
| fso.CopyFile sourcePath, CreateDisposableDatabaseCopy, True | |||||
| Set fso = Nothing | |||||
| End Function | |||||
| Private Sub UseDisposableDatabase(ByVal dbPath) | |||||
| Set DAL__Singleton = Nothing | |||||
| set DAL__Singleton = new Database_Class | |||||
| DAL__Singleton.Initialize "Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=" & dbPath & ";" | |||||
| End Sub | |||||
| Private Sub ResetDisposableDatabase() | |||||
| dim tempPath : tempPath = m_tempDbPath | |||||
| Set DAL__Singleton = Nothing | |||||
| If Len(tempPath) > 0 Then | |||||
| dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject") | |||||
| If fso.FileExists(tempPath) Then | |||||
| fso.DeleteFile tempPath, True | |||||
| End If | |||||
| Set fso = Nothing | |||||
| End If | |||||
| m_tempDbPath = "" | |||||
| End Sub | |||||
| Private Function NextSeedKey(ByVal prefix) | |||||
| NextSeedKey = prefix & Replace(Replace(Replace(CStr(Now()), "/", ""), ":", ""), " ", "") & CStr(Int((Rnd() * 1000000) + 1)) | |||||
| End Function | |||||
| Private Function NextJurisdictionCode() | |||||
| dim guidValue : guidValue = Mid(CreateObject("Scriptlet.TypeLib").Guid, 2, 8) | |||||
| NextJurisdictionCode = "J" & Replace(guidValue, "-", "") | |||||
| End Function | |||||
| Private Function SeedJurisdiction(ByVal jCode, ByVal name) | |||||
| DAL.Execute "INSERT INTO [Jurisdiction] ([JCode], [Name], [Mailing_Address], [CSZ], [IMB], [IMB_Digits]) VALUES (?,?,?,?,?,?)", _ | |||||
| Array(jCode, name, "123 Test St", "Lansing, MI 48933", "IMB", "123456789") | |||||
| SeedJurisdiction = jCode | |||||
| End Function | |||||
| Private Function SeedPurpleEnvelopeKit(ByVal jobNumber, ByVal jCode, ByVal status) | |||||
| DAL.Execute "INSERT INTO [Kit] ([JobNumber], [Jcode], [CreatedOn], [InkJetJob], [JobType], [Cass], [Status], [OutboundSTID], [InboundSTID], [OfficeCopiesAmount]) VALUES (?,?,?,?,?,?,?,?,?,?)", _ | |||||
| Array(jobNumber, jCode, Now(), False, "Purple Envelopes", False, status, "", "", Null) | |||||
| SeedPurpleEnvelopeKit = QueryScalar("SELECT TOP 1 [ID] FROM [Kit] WHERE [JobNumber] = ? ORDER BY [ID] DESC", jobNumber) | |||||
| End Function | |||||
| Private Sub SeedInkjetRecord(ByVal kitId, ByVal precinct, ByVal ballotNumber, ByVal colorId) | |||||
| DAL.Execute "INSERT INTO [InkjetRecords] ([KitID], [PRECINCT], [BALLOT_NUMBER], [ColorId]) VALUES (?,?,?,?)", _ | |||||
| Array(kitId, precinct, ballotNumber, colorId) | |||||
| End Sub | |||||
| Private Function QueryScalar(ByVal sql, ByVal params) | |||||
| dim rs : set rs = DAL.Query(sql, params) | |||||
| QueryScalar = rs(0).Value | |||||
| Destroy rs | |||||
| End Function | |||||
| Private Function QueryPrecinctColorMap(ByVal kitId) | |||||
| dim map : set map = Server.CreateObject("Scripting.Dictionary") | |||||
| dim rs : set rs = DAL.Query("SELECT [PRECINCT], [ColorId] FROM [InkjetRecords] WHERE [KitID] = ? ORDER BY [PRECINCT]", kitId) | |||||
| Do Until rs.EOF | |||||
| If IsNull(rs("ColorId")) Then | |||||
| map(rs("PRECINCT") & "") = "" | |||||
| Else | |||||
| map(rs("PRECINCT") & "") = CStr(rs("ColorId")) | |||||
| End If | |||||
| rs.MoveNext | |||||
| Loop | |||||
| Destroy rs | |||||
| set QueryPrecinctColorMap = map | |||||
| End Function | |||||
| Public Sub Test_FormatElectionLabel_Returns_Mon_YYYY_For_Valid_Date(T) | |||||
| T.AssertEqual "May-2026", PurpleEnvelopeReportHelper().FormatElectionLabel("5/26/2026"), "Expected valid dates to render as Mon-YYYY." | |||||
| End Sub | |||||
| Public Sub Test_FormatElectionLabel_Returns_Trimmed_Raw_Value_For_Invalid_Date(T) | |||||
| T.AssertEqual "not a date", PurpleEnvelopeReportHelper().FormatElectionLabel(" not a date "), "Expected invalid election dates to fall back to trimmed raw text." | |||||
| T.AssertEqual "", PurpleEnvelopeReportHelper().FormatElectionLabel(""), "Expected empty election dates to stay empty." | |||||
| End Sub | |||||
| Public Sub Test_SortPrecinctColorRows_Handles_Zero_Padded_And_Lettered_Precincts(T) | |||||
| dim rs : set rs = CreatePrecinctColorRecordset(Array( _ | |||||
| Array("12B", "2"), _ | |||||
| Array("0003", ""), _ | |||||
| Array("A1", "4"), _ | |||||
| Array("12A", "3"), _ | |||||
| Array("3", "5"), _ | |||||
| Array("0001", "1"), _ | |||||
| Array("12", "6") _ | |||||
| )) | |||||
| dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctColorRows(rs) | |||||
| T.AssertEqual "0001,0003,3,12,12A,12B,A1", PrecinctListToCsv(sorted), "Expected mixed-format precincts to sort in the current ascending order." | |||||
| Destroy rs | |||||
| Destroy sorted | |||||
| End Sub | |||||
| Public Sub Test_SortPrecinctBallotRangeRows_Preserves_Ranges_While_Sorting(T) | |||||
| dim rs : set rs = CreateBallotRangeRecordset(Array( _ | |||||
| Array("12A", 20, 29), _ | |||||
| Array("0003", 1, 10), _ | |||||
| Array("12", 11, 19), _ | |||||
| Array("A1", 30, 39) _ | |||||
| )) | |||||
| dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(rs) | |||||
| dim it : set it = sorted.Iterator() | |||||
| dim row | |||||
| T.AssertEqual "0003,12,12A,A1", PrecinctListToCsv(sorted), "Expected report rows to follow the same precinct order as the color table." | |||||
| set row = it.GetNext() | |||||
| T.AssertEqual 1, row.LowBallotNumber, "Expected the low ballot number to stay attached to precinct 0003." | |||||
| T.AssertEqual 10, row.HighBallotNumber, "Expected the high ballot number to stay attached to precinct 0003." | |||||
| Destroy rs | |||||
| Destroy sorted | |||||
| End Sub | |||||
| Public Sub Test_GetPrecinctBallotRangesByKitId_Uses_Seeded_Data(T) | |||||
| Randomize | |||||
| dim jCode : jCode = NextJurisdictionCode() | |||||
| dim jobNumber : jobNumber = NextSeedKey("JOB") | |||||
| Call SeedJurisdiction(jCode, "City of Lansing") | |||||
| dim kitId : kitId = SeedPurpleEnvelopeKit(jobNumber, jCode, "Ready To Assign STIDS") | |||||
| SeedInkjetRecord kitId, "12A", "29", Null | |||||
| SeedInkjetRecord kitId, "0003", "10", Null | |||||
| SeedInkjetRecord kitId, "12A", "20", Null | |||||
| SeedInkjetRecord kitId, "0003", "1", Null | |||||
| SeedInkjetRecord kitId, "A1", "30", Null | |||||
| dim rs : set rs = InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(kitId) | |||||
| dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(rs) | |||||
| dim it : set it = sorted.Iterator() | |||||
| dim firstRow, secondRow, thirdRow | |||||
| set firstRow = it.GetNext() | |||||
| set secondRow = it.GetNext() | |||||
| set thirdRow = it.GetNext() | |||||
| T.AssertEqual "0003,12A,A1", PrecinctListToCsv(sorted), "Expected seeded ballot ranges to sort in the current report order." | |||||
| T.AssertEqual 1, firstRow.LowBallotNumber, "Expected precinct 0003 to use the seeded minimum ballot number." | |||||
| T.AssertEqual 10, firstRow.HighBallotNumber, "Expected precinct 0003 to use the seeded maximum ballot number." | |||||
| T.AssertEqual 20, secondRow.LowBallotNumber, "Expected precinct 12A to use the seeded minimum ballot number." | |||||
| T.AssertEqual 29, secondRow.HighBallotNumber, "Expected precinct 12A to use the seeded maximum ballot number." | |||||
| T.AssertEqual 30, thirdRow.LowBallotNumber, "Expected single-row precincts to keep their ballot number as the low value." | |||||
| T.AssertEqual 30, thirdRow.HighBallotNumber, "Expected single-row precincts to keep their ballot number as the high value." | |||||
| Destroy rs | |||||
| Destroy sorted | |||||
| Set firstRow = Nothing | |||||
| Set secondRow = Nothing | |||||
| Set thirdRow = Nothing | |||||
| End Sub | |||||
| Public Sub Test_UpdateColorForKit_Updates_All_Seeded_Rows_For_The_Target_Kit(T) | |||||
| Randomize | |||||
| dim jCode : jCode = NextJurisdictionCode() | |||||
| Call SeedJurisdiction(jCode, "City of Lansing") | |||||
| dim targetKitId : targetKitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS") | |||||
| dim otherKitId : otherKitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS") | |||||
| SeedInkjetRecord targetKitId, "0003", "1", 1 | |||||
| SeedInkjetRecord targetKitId, "12A", "2", Null | |||||
| SeedInkjetRecord otherKitId, "9", "3", 4 | |||||
| InkjetRecordsRepository.UpdateColorForKit targetKitId, 7 | |||||
| dim targetMap : set targetMap = QueryPrecinctColorMap(targetKitId) | |||||
| dim otherMap : set otherMap = QueryPrecinctColorMap(otherKitId) | |||||
| T.AssertEqual "7", targetMap("0003"), "Expected kit-wide color updates to touch every row in the targeted kit." | |||||
| T.AssertEqual "7", targetMap("12A"), "Expected kit-wide color updates to touch every row in the targeted kit." | |||||
| T.AssertEqual "4", otherMap("9"), "Expected kit-wide color updates to leave other kits unchanged." | |||||
| Set targetMap = Nothing | |||||
| Set otherMap = Nothing | |||||
| End Sub | |||||
| Public Sub Test_UpdateColorForPrecinct_Updates_Only_The_Targeted_Precinct(T) | |||||
| Randomize | |||||
| dim jCode : jCode = NextJurisdictionCode() | |||||
| Call SeedJurisdiction(jCode, "City of Lansing") | |||||
| dim kitId : kitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS") | |||||
| SeedInkjetRecord kitId, "0003", "1", 1 | |||||
| SeedInkjetRecord kitId, "12A", "2", 2 | |||||
| SeedInkjetRecord kitId, "A1", "3", Null | |||||
| InkjetRecordsRepository.UpdateColorForPrecinct kitId, "12A", 9 | |||||
| dim colorMap : set colorMap = QueryPrecinctColorMap(kitId) | |||||
| T.AssertEqual "1", colorMap("0003"), "Expected non-targeted precincts to keep their original color." | |||||
| T.AssertEqual "9", colorMap("12A"), "Expected the targeted precinct to receive the new color." | |||||
| T.AssertEqual "", colorMap("A1"), "Expected non-targeted blank colors to remain blank." | |||||
| Set colorMap = Nothing | |||||
| End Sub | |||||
| Public Sub Test_SwitchBoardPurpleEnvelopeEditFindById_Returns_Seeded_Header_Data(T) | |||||
| Randomize | |||||
| dim jCode : jCode = NextJurisdictionCode() | |||||
| dim jobNumber : jobNumber = NextSeedKey("JOB") | |||||
| Call SeedJurisdiction(jCode, "City of Lansing") | |||||
| dim kitId : kitId = SeedPurpleEnvelopeKit(jobNumber, jCode, "Ready To Assign STIDS") | |||||
| SeedInkjetRecord kitId, "0003", "1", Null | |||||
| SeedInkjetRecord kitId, "12A", "2", Null | |||||
| dim model : set model = KitRepository.SwitchBoardPurpleEnvelopeEditFindById(kitId) | |||||
| T.AssertEqual kitId, model.ID, "Expected the seeded purple-envelope kit to be returned." | |||||
| T.AssertEqual jobNumber, model.JobNumber, "Expected the seeded job number to be returned." | |||||
| T.AssertEqual jCode, model.JCode, "Expected the seeded jurisdiction code to be returned." | |||||
| T.AssertEqual "City of Lansing", model.Jurisdiction, "Expected the seeded jurisdiction name to be returned." | |||||
| T.AssertEqual 2, model.LabelCount, "Expected the seeded inkjet row count to flow into the label count." | |||||
| T.AssertEqual "Ready To Assign STIDS", model.Status, "Expected the seeded status to be returned." | |||||
| Set model = Nothing | |||||
| End Sub | |||||
| Public Sub Test_KitController_Post_Actions_Still_Delegate_To_Color_Update_Repositories(T) | |||||
| dim controllerSource : controllerSource = ReadAllText("../App/Controllers/Kit/KitController.asp") | |||||
| T.Assert InStr(controllerSource, "Public Sub AssignKitColorPost") > 0, "Expected the kit-wide color POST action to remain present." | |||||
| T.Assert InStr(controllerSource, "InkjetRecordsRepository.UpdateColorForKit CLng(ID), CLng(Request.Form(""KitColorId""))") > 0, "Expected AssignKitColorPost to keep delegating to the kit-wide color repository update." | |||||
| T.Assert InStr(controllerSource, "Public Sub AssignPrecinctColorsPost") > 0, "Expected the precinct color POST action to remain present." | |||||
| T.Assert InStr(controllerSource, "InkjetRecordsRepository.UpdateColorForPrecinct CLng(ID), precinct, CLng(colorId)") > 0, "Expected AssignPrecinctColorsPost to keep delegating to the precinct-specific color repository update." | |||||
| End Sub | |||||
| Public Sub Test_Report_View_Keeps_Print_Only_CSS_Contract(T) | |||||
| dim viewSource : viewSource = ReadAllText("../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp") | |||||
| T.Assert InStr(viewSource, "id=""purple-envelope-report-print""") > 0, "Expected the report print wrapper id to remain in the view." | |||||
| T.Assert InStr(viewSource, "body * {") > 0, "Expected print CSS to hide non-report content." | |||||
| T.Assert InStr(viewSource, "font-size: 10pt;") > 0, "Expected the reduced report font size to remain unchanged." | |||||
| T.Assert InStr(viewSource, "padding: 0.45in 0.25in 0.25in 0.25in;") > 0, "Expected the print top buffer padding to remain unchanged." | |||||
| End Sub | |||||
| Public Sub Test_Report_View_Keeps_No_Data_Row_And_Page_Spacer(T) | |||||
| dim viewSource : viewSource = ReadAllText("../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp") | |||||
| T.Assert InStr(viewSource, "class=""print-page-spacer""") > 0, "Expected the repeated-page spacer row to remain in the report header." | |||||
| T.Assert InStr(viewSource, "No precinct ballot data found for this kit.") > 0, "Expected the empty-state report message to remain unchanged." | |||||
| End Sub | |||||
| End Class | |||||
| %> | |||||
| @@ -9,6 +9,7 @@ Option Explicit | |||||
| <!--#include file="TestCase_Automapper_DynMap.asp"--> | <!--#include file="TestCase_Automapper_DynMap.asp"--> | ||||
| <!--#include file="TestCase_StringBuilder.asp"--> | <!--#include file="TestCase_StringBuilder.asp"--> | ||||
| <!--#include file="TestCase_HtmlHelperDropdownLists.asp"--> | <!--#include file="TestCase_HtmlHelperDropdownLists.asp"--> | ||||
| <!--#include file="TestCase_PurpleEnvelopeReport.asp"--> | |||||
| <% | <% | ||||
| 'Used in some of the test case classes included into this file. | 'Used in some of the test case classes included into this file. | ||||
| @@ -26,5 +27,6 @@ Runner.AddTestContainer new AutoMap_Tests | |||||
| Runner.AddTestContainer new FlexMap_Tests | Runner.AddTestContainer new FlexMap_Tests | ||||
| Runner.AddTestContainer new DynMap_Tests | Runner.AddTestContainer new DynMap_Tests | ||||
| Runner.AddTestContainer new StringBuilder_Tests | Runner.AddTestContainer new StringBuilder_Tests | ||||
| Runner.AddTestContainer new PurpleEnvelopeReport_Tests | |||||
| Runner.Display | Runner.Display | ||||
| %> | %> | ||||
| @@ -0,0 +1 @@ | |||||
| KENTCM.CB1022025_RGzBPM5J655e | |||||
| @@ -0,0 +1,300 @@ | |||||
| --- | |||||
| title: 'ExportInkjetFile Integration Test' | |||||
| slug: 'exportinkjetfile-integration-test' | |||||
| created: '2026-03-17' | |||||
| status: 'Implementation Complete' | |||||
| stepsCompleted: [1, 2, 3, 4] | |||||
| tech_stack: ['VBScript', 'ADODB', 'Chilkat_9_5_0.Csv', 'Access MDB/Jet'] | |||||
| files_to_modify: ['Tests/TrackingDataImport_TestHarness.vbs'] | |||||
| code_patterns: ['LoadFunctions+ExecuteGlobal isolation', 'ADODB connection to MDB', 'Chilkat CSV read-back'] | |||||
| test_patterns: ['Integration test with real MDB fixture', 'Assert file output + DB side effect'] | |||||
| --- | |||||
| # Tech-Spec: ExportInkjetFile Integration Test | |||||
| **Created:** 2026-03-17 | |||||
| ## Overview | |||||
| ### Problem Statement | |||||
| `ExportInkjetFile(KitID)` produces the inkjet operator CSV used on press day. It has zero automated coverage. The function joins InkjetRecords + KitLabels + Colors from an Access MDB, builds a Chilkat CSV with 22 columns, writes it to disk, then marks Kit.Status='Done'. A regression here causes silent bad output with no test catching it. | |||||
| ### Solution | |||||
| Add an ADODB integration test directly to `Tests/TrackingDataImport_TestHarness.vbs`. The harness opens `Data/webdata - Copy.mdb` via ADODB, calls `ExportInkjetFile` on a real KitID, and asserts CSV structure (file exists, 22 columns, correct headers) plus ~10 row-level field values and the DB side-effect (Kit.Status='Done', InkJetJob=1). | |||||
| ### Scope | |||||
| **In Scope:** | |||||
| - Set up integration test globals in harness: `oConn`, `ConnectionString` (pointing to `Data/webdata - Copy.mdb`), `ExportDirectory` (temp path under `Tests/`) | |||||
| - Load `GetSetting` and `ExportInkjetFile` into the `functionNames` array | |||||
| - Query MDB to discover a real KitID that has InkjetRecords | |||||
| - Call `ExportInkjetFile(KitID)` | |||||
| - Assert: CSV file exists at expected path | |||||
| - Assert: 22 column headers correct (spot-check cols 0, 5, 8, 11, 19, 20, 21) | |||||
| - Assert: CSV row count = InkjetRecords count for that KitID | |||||
| - Assert: First 10 rows — "Ballot Number" has leading zeros stripped, "Matching Code" starts with JCode | |||||
| - Assert: Kit.Status = 'Done' and Kit.InkJetJob = 1 after call | |||||
| - Cleanup: delete temp export folder | |||||
| **Out of Scope:** | |||||
| - Pure logic extraction from `ExportInkjetFile` (not viable — all field assembly is inline with recordset reads; refactoring would be required) | |||||
| - Seeding fixture data (real kit + inkjet records already exist in `Data/webdata - Copy.mdb`) | |||||
| - Office copies rows (separate conditional path; can be added later if a Kit with OfficeCopiesAmount > 0 is present) | |||||
| - Full row-by-row validation (10-record spot check is sufficient) | |||||
| - Restoring Kit.Status after test (MDB copy is disposable; original `webdata.mdb` is untouched) | |||||
| --- | |||||
| ## Context for Development | |||||
| ### Codebase Patterns | |||||
| - **Harness pattern**: `LoadFunctions(filePath, functionNames)` extracts named functions from source via text parsing; `ExecuteGlobal` makes them available in harness global scope. Global vars set before calling functions are visible inside loaded functions. | |||||
| - **`LoadFunctions` is called at line 34**, before any test code. Adding `"GetSetting"` and `"ExportInkjetFile"` to the `functionNames` array (lines 16–32) is sufficient — they'll be extracted and available globally. | |||||
| - **`Set objFSO = fso` at line 36** — already set. `ExportInkjetFile` uses `objFSO` internally; this is already satisfied. | |||||
| - **`chilkatAvailable` declared at line 39** — already exists. New `integrationDbAvailable` follows the same declare-in-preamble, set-in-init-block pattern. | |||||
| - **Assertion API**: The harness has **no `Assert` sub** — only `AssertEqual(actual, expected, label)` and `AssertArrayEqual`. All integration test assertions must use `AssertEqual condition, True, "label"` form. | |||||
| - **DB dependency chain**: `ExportInkjetFile` calls `oConn.Open(ConnectionString)` if `oConn.State = 0`. It manages its own connection lifecycle. The harness must set `oConn` (via `Set oConn = CreateObject("ADODB.Connection")`) and `ConnectionString` as globals before calling. | |||||
| - **`ExportDirectory` global**: Used by `ExportInkjetFile` as-is (no trailing slash added internally). Must include trailing `\` in the harness assignment. | |||||
| - **Chilkat CSV write-back verification**: `ExportInkjetFile` creates its own internal `Chilkat_9_5_0.Csv` object. Chilkat unlock is process-wide — already done in harness init block. Read-back uses a separate object. | |||||
| ### Files to Reference | |||||
| | File | Purpose | | |||||
| | ---- | ------- | | |||||
| | `Tests/TrackingDataImport_TestHarness.vbs` | Target file — 3 insertion points: globals (after line 15), `functionNames` array (after line 31), init block (after line 69), test block (before line 206) | | |||||
| | `ImportService/TrackingDataImport.vbs` | Source — `ExportInkjetFile` at line 251 (Function), `GetSetting` also in same file | | |||||
| | `Data/webdata - Copy.mdb` | MDB fixture — real Kit + InkjetRecords + KitLabels + Colors + Jurisdiction + Contacts + Settings | | |||||
| ### Technical Decisions | |||||
| - **Use `Data/webdata - Copy.mdb` directly**: It's already a copy and contains real data. No need to seed a new fixture. The UPDATE side effect (Kit.Status='Done') is acceptable since this MDB is a disposable copy. | |||||
| - **Discover KitID at runtime via JOIN**: Query `SELECT TOP 1 ir.KitID FROM ((InkjetRecords ir INNER JOIN Kit k ON ir.KitID = k.ID) INNER JOIN Jurisdiction j ON k.JCode = j.JCode) INNER JOIN Contacts c ON k.JCode = c.JURISCODE` to ensure discovered KitID has all required related records. Avoid orphan-crash on `JurisdictionRs` or `ContactRs` inside the function. | |||||
| - **ExportDirectory as temp path**: Set to `fso.BuildPath(scriptDir, "export-test-output")`. **Delete before call** (not just after) to ensure no stale CSV from a prior failed run contaminates assertions. | |||||
| - **ADODB driver probe**: Try `Microsoft.ACE.OLEDB.12.0` first; fall back to `Microsoft.Jet.OLEDB.4.0`. If both fail, skip with message. Guard with `fso.FileExists(integrationMdbPath)` before attempting open. | |||||
| - **Load both `GetSetting` and `ExportInkjetFile`** in `functionNames` array — `GetSetting` is a DB-backed dependency called internally by `ExportInkjetFile`. | |||||
| - **Chilkat read-back**: Capture `verifyCsv.LoadFile(csvPath)` return value and assert it before proceeding to column/row assertions — prevents vacuously-passing assertions on a failed load. | |||||
| - **Cleanup is error-suppressed**: Wrap `fso.DeleteFolder` in `On Error Resume Next` / `On Error GoTo 0` — AV scanners may briefly hold a handle on the new CSV; a cleanup failure should not fail the test. | |||||
| --- | |||||
| ## Implementation Plan | |||||
| ### Tasks | |||||
| - [x] **Task 1: Add integration test globals and ADODB probe block** | |||||
| - File: `Tests/TrackingDataImport_TestHarness.vbs` | |||||
| - Action: Insert 7 `Dim` declarations after line 15; insert ADODB probe block after line 69 | |||||
| - Notes: Sets `integrationDbAvailable`/`integrationDbSkipReason` — same guard pattern as `chilkatAvailable` | |||||
| **Insertion point: after line 15** (`Dim DataDirectory`). Add: | |||||
| ```vbscript | |||||
| Dim oConn | |||||
| Dim ConnectionString | |||||
| Dim ExportDirectory | |||||
| Dim integrationMdbPath | |||||
| Dim integrationExportDir | |||||
| Dim integrationDbAvailable | |||||
| Dim integrationDbSkipReason | |||||
| ``` | |||||
| **Insertion point: after line 69** (`On Error GoTo 0` at end of Chilkat init block). Add ADODB probe block: | |||||
| ```vbscript | |||||
| integrationMdbPath = fso.BuildPath(scriptDir, "..\Data\webdata - Copy.mdb") | |||||
| integrationExportDir = fso.BuildPath(scriptDir, "export-test-output") | |||||
| integrationDbAvailable = False | |||||
| integrationDbSkipReason = "" | |||||
| If Not fso.FileExists(integrationMdbPath) Then | |||||
| integrationDbSkipReason = "MDB fixture not found: " & integrationMdbPath | |||||
| Else | |||||
| Set oConn = CreateObject("ADODB.Connection") | |||||
| On Error Resume Next | |||||
| oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & integrationMdbPath & ";" | |||||
| If Err.Number <> 0 Then | |||||
| Err.Clear | |||||
| oConn.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & integrationMdbPath & ";" | |||||
| End If | |||||
| If Err.Number <> 0 Then | |||||
| integrationDbSkipReason = "No ADODB provider available (ACE and Jet both failed): " & Err.Description | |||||
| Err.Clear | |||||
| Else | |||||
| integrationDbAvailable = True | |||||
| ConnectionString = oConn.ConnectionString | |||||
| ExportDirectory = integrationExportDir & "\" | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| If oConn.State = 1 Then oConn.Close | |||||
| End If | |||||
| ``` | |||||
| - [x] **Task 2: Add `GetSetting` and `ExportInkjetFile` to `functionNames` array** | |||||
| - File: `Tests/TrackingDataImport_TestHarness.vbs` | |||||
| - Action: Extend array at lines 30–31 to include two new entries | |||||
| - Notes: Must be done before `LoadFunctions` call at line 34; order within array does not matter | |||||
| **Insertion point: lines 30–31**. Change: | |||||
| ```vbscript | |||||
| "CheckStringDoesNotHaveForiegnCountries", _ | |||||
| "ValidImportCSV" _ | |||||
| ``` | |||||
| To: | |||||
| ```vbscript | |||||
| "CheckStringDoesNotHaveForiegnCountries", _ | |||||
| "ValidImportCSV", _ | |||||
| "GetSetting", _ | |||||
| "ExportInkjetFile" _ | |||||
| ``` | |||||
| - [x] **Task 3: Add ExportInkjetFile integration test block** | |||||
| - File: `Tests/TrackingDataImport_TestHarness.vbs` | |||||
| - Action: Insert full integration test block before line 206 (`WScript.Echo ""`) | |||||
| - Notes: Guarded by `chilkatAvailable` AND `integrationDbAvailable`; uses `AssertEqual` throughout (no `Assert` sub exists); pre-call dir cleanup, post-call DB side-effect check, error-suppressed cleanup | |||||
| **Insertion point: before line 206** (`WScript.Echo ""`). Add: | |||||
| ```vbscript | |||||
| ' === ExportInkjetFile Integration Test === | |||||
| If Not chilkatAvailable Then | |||||
| WScript.Echo "SKIP: ExportInkjetFile integration test (Chilkat unavailable)" | |||||
| ElseIf Not integrationDbAvailable Then | |||||
| WScript.Echo "SKIP: ExportInkjetFile integration test (" & integrationDbSkipReason & ")" | |||||
| Else | |||||
| oConn.Open ConnectionString | |||||
| Dim kitDiscoverRs | |||||
| Set kitDiscoverRs = oConn.Execute( _ | |||||
| "SELECT TOP 1 ir.KitID FROM ((InkjetRecords ir " & _ | |||||
| "INNER JOIN Kit k ON ir.KitID = k.ID) " & _ | |||||
| "INNER JOIN Jurisdiction j ON k.JCode = j.JCode) " & _ | |||||
| "INNER JOIN Contacts c ON k.JCode = c.JURISCODE;") | |||||
| If kitDiscoverRs.EOF Then | |||||
| WScript.Echo "SKIP: ExportInkjetFile — no complete Kit+InkjetRecords+Jurisdiction+Contact found in fixture MDB" | |||||
| oConn.Close | |||||
| Else | |||||
| Dim testKitID : testKitID = kitDiscoverRs("KitID").Value | |||||
| Dim countRs | |||||
| Set countRs = oConn.Execute("SELECT COUNT(*) AS N FROM InkjetRecords WHERE KitID=" & testKitID & ";") | |||||
| Dim expectedRows : expectedRows = countRs("N").Value | |||||
| Dim kitRsCheck | |||||
| Set kitRsCheck = oConn.Execute("SELECT JCode FROM Kit WHERE ID=" & testKitID & ";") | |||||
| Dim testJCode : testJCode = kitRsCheck("JCode").Value | |||||
| oConn.Close | |||||
| ' Delete stale output before call, then recreate | |||||
| On Error Resume Next | |||||
| If fso.FolderExists(integrationExportDir) Then fso.DeleteFolder(integrationExportDir, True) | |||||
| On Error GoTo 0 | |||||
| fso.CreateFolder integrationExportDir | |||||
| ExportInkjetFile testKitID | |||||
| Dim exportSubfolders : Set exportSubfolders = fso.GetFolder(integrationExportDir).SubFolders | |||||
| Dim csvFound : csvFound = False | |||||
| Dim csvPath : csvPath = "" | |||||
| Dim sf | |||||
| For Each sf In exportSubfolders | |||||
| Dim csvFiles : Set csvFiles = sf.Files | |||||
| Dim f | |||||
| For Each f In csvFiles | |||||
| If LCase(fso.GetExtensionName(f.Name)) = "csv" Then | |||||
| csvFound = True | |||||
| csvPath = f.Path | |||||
| End If | |||||
| Next | |||||
| Next | |||||
| AssertEqual csvFound, True, "[INT] ExportInkjetFile: CSV file created" | |||||
| If csvFound Then | |||||
| Dim verifyCsv : Set verifyCsv = CreateObject("Chilkat_9_5_0.Csv") | |||||
| verifyCsv.HasColumnNames = 1 | |||||
| Dim csvLoaded : csvLoaded = verifyCsv.LoadFile(csvPath) | |||||
| AssertEqual csvLoaded, True, "[INT] ExportInkjetFile: CSV loaded by Chilkat" | |||||
| If csvLoaded Then | |||||
| AssertEqual verifyCsv.NumColumns, 22, "[INT] ExportInkjetFile: 22 columns" | |||||
| AssertEqual verifyCsv.ColumnName(0), "Full Name", "[INT] ExportInkjetFile: col 0 = Full Name" | |||||
| AssertEqual verifyCsv.ColumnName(5), "IM barcode Characters", "[INT] ExportInkjetFile: col 5 = IM barcode Characters" | |||||
| AssertEqual verifyCsv.ColumnName(8), "Ballot Number", "[INT] ExportInkjetFile: col 8 = Ballot Number" | |||||
| AssertEqual verifyCsv.ColumnName(11), "Combined Pct_Ballot Num", "[INT] ExportInkjetFile: col 11 = Combined Pct_Ballot Num" | |||||
| AssertEqual verifyCsv.ColumnName(19), "Matching Code", "[INT] ExportInkjetFile: col 19 = Matching Code" | |||||
| AssertEqual verifyCsv.ColumnName(20), "ColorFilepath", "[INT] ExportInkjetFile: col 20 = ColorFilepath" | |||||
| AssertEqual verifyCsv.ColumnName(21), "ColorName", "[INT] ExportInkjetFile: col 21 = ColorName" | |||||
| AssertEqual verifyCsv.NumRows, expectedRows, "[INT] ExportInkjetFile: row count matches InkjetRecords" | |||||
| Dim checkRows : checkRows = 10 | |||||
| If verifyCsv.NumRows < 10 Then checkRows = verifyCsv.NumRows | |||||
| Dim r | |||||
| For r = 0 To checkRows - 1 | |||||
| Dim ballotNum : ballotNum = verifyCsv.GetCell(r, 8) | |||||
| AssertEqual (Left(ballotNum, 1) <> "0" Or ballotNum = ""), True, "[INT] ExportInkjetFile: row " & r & " Ballot Number no leading zeros" | |||||
| Dim matchCode : matchCode = verifyCsv.GetCell(r, 19) | |||||
| AssertEqual Left(matchCode, Len(testJCode)), testJCode, "[INT] ExportInkjetFile: row " & r & " Matching Code starts with JCode" | |||||
| Next | |||||
| End If | |||||
| Set verifyCsv = Nothing | |||||
| End If | |||||
| oConn.Open ConnectionString | |||||
| Dim sideEffectRs | |||||
| Set sideEffectRs = oConn.Execute("SELECT Status, InkJetJob FROM Kit WHERE ID=" & testKitID & ";") | |||||
| AssertEqual sideEffectRs("Status").Value, "Done", "[INT] ExportInkjetFile: Kit.Status = Done" | |||||
| AssertEqual sideEffectRs("InkJetJob").Value, 1, "[INT] ExportInkjetFile: Kit.InkJetJob = 1" | |||||
| oConn.Close | |||||
| On Error Resume Next | |||||
| If fso.FolderExists(integrationExportDir) Then fso.DeleteFolder(integrationExportDir, True) | |||||
| On Error GoTo 0 | |||||
| End If | |||||
| End If | |||||
| ``` | |||||
| - [x] **Task 4: Verify `Option Explicit` compliance** | |||||
| - File: `Tests/TrackingDataImport_TestHarness.vbs` | |||||
| - Action: Review all new variable names introduced in Tasks 1–3 and confirm each has a `Dim` declaration | |||||
| - Notes: New inline `Dim`s in Task 3: `kitDiscoverRs`, `testKitID`, `countRs`, `expectedRows`, `kitRsCheck`, `testJCode`, `exportSubfolders`, `csvFound`, `csvPath`, `sf`, `csvFiles`, `f`, `verifyCsv`, `csvLoaded`, `checkRows`, `r`, `ballotNum`, `matchCode`, `sideEffectRs` — all must be present | |||||
| ### Acceptance Criteria | |||||
| - [ ] **AC 1:** Given Chilkat is available and `Data/webdata - Copy.mdb` contains a Kit with InkjetRecords, Jurisdiction, and Contact, when the harness calls `ExportInkjetFile(testKitID)`, then a CSV file is created under `Tests/export-test-output/<JobNumber>-<Name>/`, it has exactly 22 columns with correct header names at indices 0/5/8/11/19/20/21, row count matches the InkjetRecords count, the first 10 rows have no leading zeros in "Ballot Number", the first 10 rows have "Matching Code" starting with JCode, and Kit.Status='Done' + Kit.InkJetJob=1 in the MDB. | |||||
| - [ ] **AC 2:** Given Chilkat is unavailable, when the harness reaches the ExportInkjetFile block, then it prints `SKIP: ExportInkjetFile integration test (Chilkat unavailable)` and no test failures are recorded. | |||||
| - [ ] **AC 3:** Given `Data/webdata - Copy.mdb` is absent or neither ACE nor Jet ADODB driver is available, when the ADODB probe block runs during harness init, then `integrationDbAvailable` is False, the test block prints a SKIP message with the reason, and no test failures are recorded. | |||||
| - [ ] **AC 4:** Given the MDB contains no Kit with all required related records (InkjetRecords + Jurisdiction + Contacts), when the discovery JOIN query runs, then it prints `SKIP: ExportInkjetFile — no complete Kit+InkjetRecords+Jurisdiction+Contact found in fixture MDB` and no test failures are recorded. | |||||
| - [ ] **AC 5:** Given a prior test run left `Tests/export-test-output/` behind (simulating a failed cleanup), when the integration test runs, then the stale folder is deleted before `ExportInkjetFile` is called and the test proceeds without contaminated assertions. | |||||
| --- | |||||
| ## Additional Context | |||||
| ### Dependencies | |||||
| - `Chilkat_9_5_0.Csv` COM — must be registered and unlocked (guarded by `chilkatAvailable`) | |||||
| - `Microsoft.ACE.OLEDB.12.0` or `Microsoft.Jet.OLEDB.4.0` — must be available for ADODB to open the MDB | |||||
| - `Data/webdata - Copy.mdb` — must exist with real Kit + InkjetRecords + KitLabels + Colors + Jurisdiction + Contacts + Settings(ElectionDate) | |||||
| - `ADODB.Connection` — available on any Windows machine with Office/Access drivers | |||||
| ### Testing Strategy | |||||
| Integration test only — no unit-level testing is viable for this function without significant refactoring. The test confirms the full output pipeline from DB read → CSV write → DB update. | |||||
| ### Notes | |||||
| - The `ExportInkjetFile` function uses `objFSO` (not `fso`). The line `Set objFSO = fso` was added in a prior workflow pass and is already present in the harness. | |||||
| - Column index 11 has a double-space in its name: `"Combined Pct_Ballot Num"` — this is as-written in the source (line 296). | |||||
| - The Chilkat CSV column indices: source sets cols 0–21 (22 columns total). The `ColumnName(n)` method on the read-back CSV object uses 0-based index. | |||||
| - **ADODB driver probe**: Init block tries ACE first (`Microsoft.ACE.OLEDB.12.0`), falls back to Jet (`Microsoft.Jet.OLEDB.4.0`). Bitness matters — 32-bit cscript requires 32-bit drivers. Sets `integrationDbAvailable = True/False` and `integrationDbSkipReason`. | |||||
| - **oConn lifecycle**: The probe block opens and immediately closes `oConn` to validate the connection string. `ExportInkjetFile` internally calls `oConn.Open(ConnectionString)` if State=0 — so close it before calling the function. Reopen after for the side-effect assertion. | |||||
| - **KitID discovery JOIN**: Uses 4-table JOIN (InkjetRecords + Kit + Jurisdiction + Contacts) to guarantee no orphan-crash inside `ExportInkjetFile` when it accesses `JurisdictionRs("Name")` or `ContactRs("Title")`. | |||||
| - **Pre-call cleanup**: `integrationExportDir` is deleted before calling `ExportInkjetFile` (not just after) to prevent stale CSVs from prior failed runs contaminating assertions. | |||||
| @@ -0,0 +1,187 @@ | |||||
| --- | |||||
| stepsCompleted: | |||||
| - step-01-preflight-and-context | |||||
| - step-02-identify-targets | |||||
| - step-03-generate-tests | |||||
| - step-03c-aggregate | |||||
| - step-04-validate-and-summarize | |||||
| lastStep: step-04-validate-and-summarize | |||||
| lastSaved: 2026-03-17 | |||||
| workflowType: testarch-automate | |||||
| target: ImportService/TrackingDataImport.vbs | |||||
| --- | |||||
| # Test Automation Expansion: TrackingDataImport.vbs | |||||
| ## Step 1: Preflight & Context | |||||
| ### Stack Detection | |||||
| - `test_stack_type`: auto → **detected: `backend`** | |||||
| - No frontend tooling (no `package.json`, no Playwright/Cypress config) | |||||
| - No conventional backend manifests — project is **VBScript / Classic ASP** | |||||
| - Framework for this component: **standalone VBScript test harness** (`Tests/TrackingDataImport_TestHarness.vbs`) run via `cscript` | |||||
| ### Execution Mode | |||||
| **Standalone** — source code only; no story/PRD/test-design artifacts exist for this component. | |||||
| ### TEA Config Flags | |||||
| - `tea_use_playwright_utils`: true → **Not applicable** (no browser/HTTP layer) | |||||
| - `tea_use_pactjs_utils`: true → **Not applicable** (no service contracts) | |||||
| - `tea_pact_mcp`: mcp → **Not applicable** | |||||
| - `tea_browser_automation`: auto → **Not applicable** (script only) | |||||
| ### Existing Test Harness Summary | |||||
| `Tests/TrackingDataImport_TestHarness.vbs` uses `LoadFunctions` + `ExecuteGlobal` to extract individual functions from the source and test them in isolation. Current state: | |||||
| | Function | Status | Test Count | | |||||
| |---|---|---| | |||||
| | `Truncate` | ✅ Tested | 2 | | |||||
| | `PadLeft` | ✅ Tested | 2 | | |||||
| | `PadString` | ✅ Tested | 3 (including Null input) | | |||||
| | `CleanNull` | ✅ Tested | 2 | | |||||
| | `CompressArray` | ✅ Tested | 1 | | |||||
| | `TrimLeadingZeros` | ✅ Tested | 2 | | |||||
| | `PushNonEmptyToBottom` | ✅ Tested | 1 | | |||||
| | `GetState` | ✅ Tested | 2 | | |||||
| | `GetCityFromLine` | ✅ Tested | 3 (including Null) | | |||||
| | `Assign` | ⚠️ Loaded, not tested | 0 | | |||||
| | `Choice` | ⚠️ Loaded, not tested | 0 | | |||||
| | `CheckForFiles` | ⚠️ Smoke only (empty dir) | 1 | | |||||
| | `CheckStringDoesNotHaveForiegnCountries` | ❌ Not in harness | 0 | | |||||
| ### Functions Out of Scope (Require COM/DB/Shell) | |||||
| `ValidJcode`, `GetSetting`, `CheckStatusFor`, `CheckForJobsToCass`, `ValidImportCSV`, `ConvertCsvToString`, `SetupKit`, `ImportCass`, `ExportMMCsv`, `RunMailManager`, `CreateExportForSnailWorks`, `CreateProofForJurisdiction`, `createTrackingInfoForKit`, `ExportInkjetFile`, `ThereAreCustomOfficeCopyJobsReady`, `CreateCustomOfficeCopyJobsInKjetFiles`, `CreateCustomOfficeCopyJobsProofFiles`, `CheckSnailWorksPurpleEnvelopeExport`, `CheckSnailWorksTrakingKitExport`, `Main`, `InitConfig`, `ProcessStatus` | |||||
| --- | |||||
| ## Step 2: Coverage Plan | |||||
| ### ⚠️ Highest Actual Risk — Zero Coverage (Orchestration Path) | |||||
| The import pipeline `CheckForFiles` → `ConvertCsvToString` → `ValidImportCSV` → `SetupKit` has **no automated coverage at any level**. This is the path that processes real CSV files into the database. A failure here causes silent data loss or incorrect kit creation. The unit tests below are baseline documentation; they do not protect this pipeline. | |||||
| ### Targets by Test Level | |||||
| **Unit (VBScript `cscript` harness) — only applicable level for this component** | |||||
| #### P0 — Critical: Import gate + behavior-documentation | |||||
| | Target | Gap | Test Cases | Note | | |||||
| |---|---|---|---| | |||||
| | `ValidImportCSV` | Import gate — accepts/rejects all incoming files; Chilkat COM likely available in harness | 20-column CSV → True, 19-column → False, 0-column → False | Investigate: harness can `CreateObject("Chilkat_9_5_0.Csv")` directly | | |||||
| | `CheckStringDoesNotHaveForiegnCountries` | Not in harness; pure logic, no COM dependency | clean US address → True, `"CANADA"` → False, `"JAPAN"` → False, lowercase `"canada"` → True (documents case-sensitive behavior) | Substring test `"123 Norway Ave"` → True documents assumption that input arrives pre-uppercased | | |||||
| | `GetState` | `IgnoreCase=False` — mixed-case CSV input silently returns no state | `"lansing, mi 48906"` → `""` (documents real behavior risk) | Affects downstream CASS processing | | |||||
| #### P0 — Failure mode cases (surfaced by FMA) | |||||
| | Target | Case | Expected | Risk Note | | |||||
| |---|---|---|---| | |||||
| | `CheckStringDoesNotHaveForiegnCountries` | Null input | type mismatch (documents crash) | `InStr(Null,x)` raises runtime error | | |||||
| | `GetState` | Null input | type mismatch | `RegExp.Execute(Null)` raises type mismatch | | |||||
| | `Choice` | Null condition | False branch taken silently | VBScript evaluates `If Null Then` as False | | |||||
| #### P1 — Important: Edge cases for already-tested functions | |||||
| | Target | Missing Edge Cases | | |||||
| |---|---| | |||||
| | `TrimLeadingZeros` | `""` → `""`, `"abc"` → `"abc"`, `"0"` → `""`, `" 007"` (leading space) → `" 007"`, Null → type mismatch | | |||||
| | `PadLeft` | exact-length `"007"` → `"007"`, empty string → `"000"`, Null → type mismatch | | |||||
| | `PadString` | longer-than-size `"abcde"` → `"abcde"`, `""` → `" "` | | |||||
| | `CompressArray` | all-empty array, all-non-empty array, single-element array | | |||||
| | `GetState` | `""` → `""` | | |||||
| #### P2 — Low priority (loaded but trivially low-risk) | |||||
| | Target | Test Cases | Note | | |||||
| |---|---|---| | |||||
| | `Assign` | scalar string, scalar number, empty string, zero | 5-line utility; tests are baseline documentation only | | |||||
| | `Choice` | True branch, False branch, computed True/False | Calls `Assign`; tests document expected delegation | | |||||
| | `CheckForFiles` deeper | Requires Chilkat CSV COM for valid-file path | Skip unless `ValidImportCSV` Chilkat investigation succeeds | | |||||
| ### Justification | |||||
| `ValidImportCSV` is the import gate — every CSV import decision flows through it. Its column-count check (`NumColumns = 20`) is testable if `Chilkat_9_5_0.Csv` can be instantiated in the harness via `CreateObject`. `CheckStringDoesNotHaveForiegnCountries` and `GetState` document real behavior risks around case sensitivity and substring matching in production CSV data. `Assign`/`Choice` are downgraded to P2 — baseline documentation, not safety-critical coverage. | |||||
| --- | |||||
| ## Step 3: Generated Tests | |||||
| ### File Modified | |||||
| `Tests/TrackingDataImport_TestHarness.vbs` — updated in-place (VBScript harness pattern, no new files required) | |||||
| ### Changes Applied | |||||
| **New globals added:** | |||||
| - `Dim objCSV` — makes Chilkat CSV available to `ValidImportCSV` | |||||
| - `Set objFSO = fso` — ensures `objFSO` is set early for loaded functions | |||||
| - `Dim chilkatAvailable` + COM probe block — skips `ValidImportCSV` tests gracefully if Chilkat not registered | |||||
| **New entries in `functionNames` array:** | |||||
| - `"CheckStringDoesNotHaveForiegnCountries"` | |||||
| - `"ValidImportCSV"` | |||||
| **New test assertions (38 new tests):** | |||||
| | Priority | Function | Cases | | |||||
| |---|---|---| | |||||
| | P0 | `CheckStringDoesNotHaveForiegnCountries` | 7 (clean US, CANADA, JAPAN, Norway Ave, lowercase canada, empty, Null) | | |||||
| | P0 | `GetState` | 2 (lowercase miss, empty) | | |||||
| | P0 | `Choice` | 1 (Null condition) | | |||||
| | P0 | `ValidImportCSV` | 3 (20-col accept, 19-col reject, empty reject) — conditional on Chilkat | | |||||
| | P1 | `TrimLeadingZeros` | 4 (empty, non-numeric, single zero, leading space) | | |||||
| | P1 | `PadLeft` | 2 (exact length, empty string) | | |||||
| | P1 | `PadString` | 2 (longer-than-size, empty string) | | |||||
| | P1 | `CompressArray` | 3 (all-empty, all-non-empty, single-element) | | |||||
| | P2 | `Assign` | 4 (string, number, empty, zero) | | |||||
| | P2 | `Choice` | 4 (True, False, computed True, computed False) | | |||||
| **Total new tests: 32** (+ 3 conditional on Chilkat availability = 35 max) | |||||
| ### Test Count Summary | |||||
| | | Count | | |||||
| |---|---| | |||||
| | Existing tests (before this pass) | 19 | | |||||
| | New tests added | 32 (+3 conditional) | | |||||
| | **Total (Chilkat available)** | **54** | | |||||
| | **Total (Chilkat unavailable)** | **51** | | |||||
| ## Validation | |||||
| ### Checklist Result: ✅ PASS | |||||
| All applicable items passed. Full checklist adapted for VBScript `cscript` harness (Playwright/TypeScript items marked N/A). | |||||
| **Key verifications:** | |||||
| - `Choice(Null, "yes", "no")` → `"no"` confirmed: VBScript `If Null Then` evaluates False | |||||
| - `CheckStringDoesNotHaveForiegnCountries(Null)` → `True` confirmed: `InStr(Null, x)` returns Null (falsy in If) | |||||
| - `ValidImportCSV` guard pattern correct: `chilkatAvailable` flag skips gracefully if COM absent | |||||
| - All `Dim` declarations satisfy `Option Explicit` requirement | |||||
| - No shared mutable state between test groups | |||||
| ### Assumptions and Risks | |||||
| - `ValidImportCSV` tests depend on `Chilkat_9_5_0.Csv` COM registration on the machine running the harness. The guard skips them cleanly if absent. | |||||
| - The orchestration path (`CheckForFiles` → `ConvertCsvToString` → `ValidImportCSV` → `SetupKit`) remains **without automated coverage** — this is the highest actual risk. Database-backed integration tests would require a seeded MDB fixture and are out of scope for the cscript harness pattern. | |||||
| - `CheckStringDoesNotHaveForiegnCountries` substring and case-sensitivity tests are **behavior-documentation tests**, not defect tests. They document the assumption that upstream CSV data arrives pre-uppercased. | |||||
| - `GetState` lowercase test documents a known production risk: mixed-case city/state lines in real CSV data silently return no state. | |||||
| ### How to Run | |||||
| ``` | |||||
| cscript Tests\TrackingDataImport_TestHarness.vbs | |||||
| ``` | |||||
| Expected output (Chilkat available): `Passed: 54, Failed: 0` | |||||
| Expected output (Chilkat unavailable): `SKIP: ValidImportCSV...` then `Passed: 51, Failed: 0` | |||||
| ### Recommended Next Workflow | |||||
| - `/bmad-tea-bmad-testarch-test-review` — review coverage quality and identify remaining gaps | |||||
| - Optional follow-up: add seeded MDB integration tests for the `CheckForFiles` → `SetupKit` pipeline using a disposable MDB copy (highest actual risk, currently uncovered) | |||||
| @@ -0,0 +1,248 @@ | |||||
| --- | |||||
| stepsCompleted: | |||||
| - step-01-detect-mode | |||||
| - step-02-load-context | |||||
| - step-03-risk-and-testability | |||||
| - step-04-coverage-plan | |||||
| - step-05-generate-output | |||||
| lastStep: step-05-generate-output | |||||
| lastSaved: 2026-03-17 | |||||
| workflowType: testarch-test-design | |||||
| inputDocuments: | |||||
| - D:\Development\Tracking_Kits\docs\project-overview.md | |||||
| - D:\Development\Tracking_Kits\docs\architecture.md | |||||
| - D:\Development\Tracking_Kits\docs\module-map.md | |||||
| - D:\Development\Tracking_Kits\docs\development-guide.md | |||||
| - D:\Development\Tracking_Kits\_bmad-output\project-context.md | |||||
| - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp | |||||
| - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp | |||||
| - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp | |||||
| --- | |||||
| # Test Design for Architecture: Purple Envelope Edit and Ballot Range Report | |||||
| **Purpose:** Architectural concerns, testability gaps, and NFR requirements for review by Dev/Architecture before adding regression tests for the current purple-envelope workflow. | |||||
| **Date:** 2026-03-17 | |||||
| **Author:** Daniel Covington | |||||
| **Status:** Architecture Review Pending | |||||
| **Project:** workspace | |||||
| **PRD Reference:** Brownfield functional references: `docs/project-overview.md`, `docs/module-map.md`, and the currently deployed purple-envelope behavior | |||||
| **ADR Reference:** Brownfield architecture references: `docs/architecture.md` and `App/Controllers/Kit/KitController.asp` | |||||
| --- | |||||
| ## Executive Summary | |||||
| **Scope:** Preserve current behavior for `SwitchBoardPurpleEnvelopeEdit`, including report rendering, mixed-format precinct ordering, election-date formatting, color-assignment forms, and print-only report scoping. | |||||
| **Business Context**: | |||||
| - **Revenue/Impact:** Operational election-mail output; wrong report data can cause incorrect ballot packaging and operator rework. | |||||
| - **Problem:** The feature is currently exercised mostly through manual IIS verification, so small controller/view/repository regressions can silently change report output. | |||||
| - **GA Launch:** Immediate brownfield regression protection for current production behavior. | |||||
| **Architecture**: | |||||
| - **Key Decision 1:** Purple-envelope behavior remains in Classic ASP controller/view/repository layers, not a separate service. | |||||
| - **Key Decision 2:** Report content is rendered as HTML from `KitController` + `InkjetRecordsRepository` + `SwitchBoardPurpleEnvelopeEdit.asp`. | |||||
| - **Key Decision 3:** Mixed-format precinct ordering is handled in VBScript controller helpers, while ballot range aggregation is still SQL-backed. | |||||
| **Expected Scale**: | |||||
| - Single-kit page render with precinct rows sourced from `InkjetRecords` | |||||
| - Operator-facing print workflow from browser print preview | |||||
| - Existing automated test harness limited to ASPUnit helper-style tests | |||||
| **Risk Summary:** | |||||
| - **Total risks:** 7 | |||||
| - **High-priority (>=6):** 3 risks requiring mitigation before relying on automated regression coverage | |||||
| - **Test effort:** ~14-19 targeted automated checks plus 2 manual print checks (~1-1.5 QA weeks) | |||||
| --- | |||||
| ## Quick Guide | |||||
| ### BLOCKERS - Team Must Decide | |||||
| 1. **R-006: No isolated fixture path for Kit/Inkjet/Settings data** - Tests need a disposable MDB or deterministic seed/reset strategy before meaningful controller/repository regression coverage can be added. Recommended owner: Dev. | |||||
| 2. **R-002: Ballot range correctness is data-sensitive** - We need agreed fixture data covering numeric, zero-padded, and alpha-suffixed precincts plus min/max ballot scenarios. Recommended owner: Dev + QA. | |||||
| 3. **R-001: Mixed-format precinct ordering is custom logic** - The current order rules must be frozen as explicit expected outputs before tests are written. Recommended owner: Product/Dev/QA. | |||||
| ### HIGH PRIORITY - Team Should Validate | |||||
| 1. **R-003: Election date formatting/fallback behavior** - Approve the rule set: valid dates render `Mon-YYYY`, invalid or missing values fall back without crashing. | |||||
| 2. **R-004: Print-only report scope depends on browser print behavior** - Approve a split strategy: automated HTML/CSS assertions plus manual browser print smoke for final confidence. | |||||
| 3. **R-005: Color-assignment regressions can hide behind the same screen** - Validate that the report tests must also preserve existing `AssignKitColorPost` and `AssignPrecinctColorsPost` behavior. | |||||
| ### INFO ONLY - Solutions Provided | |||||
| 1. **Test strategy:** favor repository/controller integration checks plus limited manual IIS print verification. | |||||
| 2. **Tooling:** reuse ASPUnit, a disposable test MDB copy, and HTML response assertions. | |||||
| 3. **Coverage:** focus first on sorting, aggregation, date formatting, same-screen regression, and print scoping. | |||||
| 4. **Gate guidance:** P0 automated checks must pass 100%; P1 >=95%; manual IIS print smoke required before release for print-impacting changes. | |||||
| --- | |||||
| ## For Architects and Devs - Open Topics | |||||
| ### Risk Assessment | |||||
| **Total risks identified:** 7 (3 high-priority score >=6, 4 medium, 0 low) | |||||
| #### High-Priority Risks (Score >=6) - Immediate Attention | |||||
| | Risk ID | Category | Description | Probability | Impact | Score | Mitigation | Owner | Timeline | | |||||
| | --- | --- | --- | --- | --- | --- | --- | --- | --- | | |||||
| | **R-001** | **DATA** | Mixed-format precinct sort order changes (`0003`, `12A`, alpha-only values) and produces wrong operator-facing sequence | 2 | 3 | **6** | Lock expected order with seeded regression fixtures and HTML assertions | Dev + QA | Before next release | | |||||
| | **R-002** | **DATA** | Ballot range query/regression returns wrong low or high ballot numbers per precinct | 2 | 3 | **6** | Add repository-level aggregation checks against known datasets | Dev + QA | Before next release | | |||||
| | **R-006** | **TECH** | No isolated test-data harness for `Kit`, `InkjetRecords`, `Settings`, and `Colors` blocks stable regression coverage | 3 | 2 | **6** | Create disposable MDB copy and reset/seeding conventions for tests | Dev | Before automated suite work | | |||||
| #### Medium-Priority Risks (Score 3-5) | |||||
| | Risk ID | Category | Description | Probability | Impact | Score | Mitigation | Owner | | |||||
| | --- | --- | --- | --- | --- | --- | --- | --- | | |||||
| | R-003 | BUS | Election date header shows wrong format or blank value unexpectedly | 2 | 2 | 4 | Add valid, invalid, and missing-setting render checks | QA | | |||||
| | R-004 | OPS | Print-only output regresses and shows extra page content or loses repeated-page spacing | 2 | 2 | 4 | Add HTML/CSS assertions and manual print smoke | QA | | |||||
| | R-005 | BUS | Same-screen changes break existing color-assignment workflows while report tests are added | 2 | 2 | 4 | Add regression checks for both POST actions | Dev + QA | | |||||
| | R-007 | BUS | Empty precinct data renders broken or confusing output instead of the current no-data row | 2 | 2 | 4 | Add explicit empty-state render test | QA | | |||||
| #### Risk Category Legend | |||||
| - **TECH**: architectural or testability risk | |||||
| - **DATA**: incorrect ballot or precinct data interpretation | |||||
| - **BUS**: operator-facing workflow correctness | |||||
| - **OPS**: environment or print/runtime behavior | |||||
| --- | |||||
| ### Testability Concerns and Architectural Gaps | |||||
| #### 1. Blockers to Fast Feedback | |||||
| | Concern | Impact | What Architecture Must Provide | Owner | Timeline | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | **No disposable test database pattern** | Automated tests will share mutable data and become brittle | A documented temp MDB copy/reset approach for ASPUnit-backed integration tests | Dev | Pre-implementation | | |||||
| | **No render-test seam for controller output** | Report assertions stay manual and expensive | A small harness or convention for invoking `SwitchBoardPurpleEnvelopeEdit` against seeded data and asserting HTML | Dev | Pre-implementation | | |||||
| | **Implicit behavior contract only lives in code/user feedback** | Tests may encode the wrong business rule | A short frozen rules list for sorting, header formatting, and empty-state expectations | Dev + QA + Stakeholders | Pre-implementation | | |||||
| #### 2. Architectural Improvements Needed | |||||
| 1. **Stabilize feature fixtures** | |||||
| - **Current problem:** no reusable seed set for mixed precinct and ballot-range cases. | |||||
| - **Required change:** create named fixture datasets for numeric-only, zero-padded, alpha-suffixed, alpha-only, and empty-state kits. | |||||
| - **Impact if not fixed:** tests will either miss key edge cases or rely on hand-built ad hoc data. | |||||
| - **Owner:** Dev | |||||
| - **Timeline:** before automated test implementation | |||||
| 2. **Separate deterministic logic from page orchestration where practical** | |||||
| - **Current problem:** ordering/date helpers are private controller methods, making low-level regression checks harder. | |||||
| - **Required change:** either expose deterministic helper seams or cover the helpers through stable page-render assertions. | |||||
| - **Impact if not fixed:** coverage will skew toward broad render tests only. | |||||
| - **Owner:** Dev | |||||
| - **Timeline:** implementation phase if needed | |||||
| 3. **Document browser-dependent print boundaries** | |||||
| - **Current problem:** browser headers/footers and repeated-page spacing are only partially controllable in app code. | |||||
| - **Required change:** record which print expectations are automatable versus manual. | |||||
| - **Impact if not fixed:** release disputes on “test passed but print preview changed”. | |||||
| - **Owner:** QA | |||||
| - **Timeline:** before sign-off | |||||
| --- | |||||
| ### Testability Assessment Summary | |||||
| #### What Works Well | |||||
| - Existing repository boundaries make ballot-range aggregation testable without full browser automation. | |||||
| - The report HTML is deterministic from seeded data, which is good for response-body assertions. | |||||
| - The feature is localized to `KitController`, `InkjetRecordsRepository`, `KitViewModels`, and one view template. | |||||
| - ASPUnit already exists, so the project has a place to add targeted regression checks. | |||||
| #### Accepted Trade-offs | |||||
| - Browser-generated print headers/footers will remain manual-verification territory. | |||||
| - Full visual print fidelity on Windows/IIS is accepted as a release smoke check, not a unit-level assertion target. | |||||
| --- | |||||
| ### Risk Mitigation Plans (High-Priority Risks >=6) | |||||
| #### R-001: Mixed-format precinct sort regression (Score: 6) - HIGH | |||||
| **Mitigation Strategy:** | |||||
| 1. Define the expected canonical sort order for representative precinct values. | |||||
| 2. Seed one kit containing `0001`, `0003`, `12`, `12A`, `12B`, and alpha-only precincts. | |||||
| 3. Assert that both the color table and report table render in the same expected order. | |||||
| **Owner:** Dev + QA | |||||
| **Timeline:** Before next release | |||||
| **Status:** Planned | |||||
| **Verification:** Automated HTML assertions against the seeded render output | |||||
| #### R-002: Ballot range aggregation regression (Score: 6) - HIGH | |||||
| **Mitigation Strategy:** | |||||
| 1. Seed multiple `InkjetRecords` rows per precinct with known min/max ballot numbers. | |||||
| 2. Assert repository output for each precinct’s low/high values. | |||||
| 3. Assert the rendered HTML shows the same values and no cross-precinct bleed. | |||||
| **Owner:** Dev + QA | |||||
| **Timeline:** Before next release | |||||
| **Status:** Planned | |||||
| **Verification:** Repository integration checks plus page-render verification | |||||
| #### R-006: Missing isolated test-data harness (Score: 6) - HIGH | |||||
| **Mitigation Strategy:** | |||||
| 1. Stand up a disposable test MDB copy for ASPUnit/controller integration runs. | |||||
| 2. Create reset hooks for `Kit`, `InkjetRecords`, `Settings`, and `Colors`. | |||||
| 3. Document fixture naming and cleanup rules for parallel-safe future tests. | |||||
| **Owner:** Dev | |||||
| **Timeline:** Before automated suite expansion | |||||
| **Status:** Planned | |||||
| **Verification:** Test runs can seed, execute, and reset without polluting shared data | |||||
| --- | |||||
| ### Assumptions and Dependencies | |||||
| #### Assumptions | |||||
| 1. Brownfield docs plus current user-approved behavior are sufficient substitutes for a formal PRD for this feature slice. | |||||
| 2. IIS-backed manual verification remains available for final print checks. | |||||
| 3. Existing status strings and form routing remain part of the contract and should not be normalized during test work. | |||||
| #### Dependencies | |||||
| 1. Disposable test data store available before controller/repository automation begins. | |||||
| 2. Mixed-format precinct fixture definitions agreed before expected-order assertions are written. | |||||
| 3. QA has access to a Windows/IIS browser for manual print smoke validation. | |||||
| #### Risks to Plan | |||||
| - **Risk:** undocumented operator expectations about sort order | |||||
| - **Impact:** the suite may freeze the wrong behavior | |||||
| - **Contingency:** validate the expected fixture output with stakeholders before coding assertions | |||||
| --- | |||||
| **End of Architecture Document** | |||||
| **Next Steps for Architecture/Dev Team:** | |||||
| 1. Approve the sort-order and date-format behavior contract. | |||||
| 2. Provide a disposable MDB fixture/reset approach. | |||||
| 3. Confirm manual print checks remain part of release validation for this feature. | |||||
| **Next Steps for QA Team:** | |||||
| 1. Use the companion QA doc for concrete scenario planning. | |||||
| 2. Build seeded datasets around mixed-format precincts and ballot ranges. | |||||
| 3. Add manual print preview checks only for browser-dependent behavior. | |||||
| @@ -0,0 +1,80 @@ | |||||
| --- | |||||
| stepsCompleted: | |||||
| - step-01-detect-mode | |||||
| - step-02-load-context | |||||
| - step-03-risk-and-testability | |||||
| - step-04-coverage-plan | |||||
| - step-05-generate-output | |||||
| lastStep: step-05-generate-output | |||||
| lastSaved: 2026-03-17 | |||||
| workflowType: testarch-test-design | |||||
| inputDocuments: | |||||
| - D:\Development\Tracking_Kits\docs\project-overview.md | |||||
| - D:\Development\Tracking_Kits\docs\architecture.md | |||||
| - D:\Development\Tracking_Kits\docs\module-map.md | |||||
| - D:\Development\Tracking_Kits\docs\development-guide.md | |||||
| - D:\Development\Tracking_Kits\_bmad-output\project-context.md | |||||
| - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp | |||||
| - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp | |||||
| - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp | |||||
| - D:\Development\Tracking_Kits\Tests\Test_All.asp | |||||
| - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\adr-quality-readiness-checklist.md | |||||
| - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\risk-governance.md | |||||
| - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\probability-impact.md | |||||
| - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-levels-framework.md | |||||
| - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-priorities-matrix.md | |||||
| - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-quality.md | |||||
| --- | |||||
| # Test Design Progress | |||||
| ## Step 1 - Detect Mode | |||||
| - Selected **system-level mode** | |||||
| - Reason: user requested a risk-based plan for current functionality, no sprint-status artifact exists, and brownfield architecture/docs were available | |||||
| - Constraint noted: no formal PRD/ADR exists for this feature slice, so brownfield docs and current approved behavior were used as the functional baseline | |||||
| ## Step 2 - Load Context | |||||
| - Loaded project context, module docs, development/testing guidance, and the exact purple-envelope controller/repository/view files | |||||
| - Loaded TEA knowledge fragments for ADR readiness, risk scoring, probability/impact, test levels, priorities, and test quality | |||||
| - Confirmed current test framework is ASPUnit with helper-focused coverage, not browser E2E automation | |||||
| ## Step 3 - Risk and Testability | |||||
| - Identified 7 risks total, with 3 high-priority risks: | |||||
| - R-001 mixed-format precinct ordering regression | |||||
| - R-002 ballot range aggregation regression | |||||
| - R-006 missing isolated test-data harness | |||||
| - Identified primary testability blockers: | |||||
| - no disposable test DB convention | |||||
| - no controller render harness | |||||
| - behavior contract is mostly implicit in code and operator expectations | |||||
| ## Step 4 - Coverage Plan | |||||
| - Defined a focused regression plan: | |||||
| - P0 ~4 checks | |||||
| - P1 ~6 checks | |||||
| - P2 ~4 checks | |||||
| - P3 ~2 manual print checks | |||||
| - Chose integration-style ASPUnit/IIS verification as the main automated level | |||||
| - Kept print-preview specifics as manual smoke for browser-controlled behavior | |||||
| ## Step 5 - Generate Output | |||||
| - Wrote architecture-focused test design: | |||||
| - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md` | |||||
| - Wrote QA execution plan: | |||||
| - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md` | |||||
| - Wrote BMAD handoff: | |||||
| - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design\workspace-handoff.md` | |||||
| ## Completion Report | |||||
| - **Mode used:** system-level | |||||
| - **Key gate thresholds:** P0 100% pass, P1 >=95%, manual Windows print smoke for print-affecting changes | |||||
| - **Open assumptions:** | |||||
| - brownfield docs are acceptable substitutes for a formal PRD | |||||
| - a disposable MDB/reset approach will be created before automation work starts | |||||
| - browser header/footer behavior remains outside reliable app-level automation | |||||
| @@ -0,0 +1,311 @@ | |||||
| --- | |||||
| stepsCompleted: | |||||
| - step-01-detect-mode | |||||
| - step-02-load-context | |||||
| - step-03-risk-and-testability | |||||
| - step-04-coverage-plan | |||||
| - step-05-generate-output | |||||
| lastStep: step-05-generate-output | |||||
| lastSaved: 2026-03-17 | |||||
| workflowType: testarch-test-design | |||||
| inputDocuments: | |||||
| - D:\Development\Tracking_Kits\docs\project-overview.md | |||||
| - D:\Development\Tracking_Kits\docs\architecture.md | |||||
| - D:\Development\Tracking_Kits\docs\module-map.md | |||||
| - D:\Development\Tracking_Kits\docs\development-guide.md | |||||
| - D:\Development\Tracking_Kits\_bmad-output\project-context.md | |||||
| - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp | |||||
| - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp | |||||
| - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp | |||||
| - D:\Development\Tracking_Kits\Tests\Test_All.asp | |||||
| --- | |||||
| # Test Design for QA: Purple Envelope Edit and Ballot Range Report | |||||
| **Purpose:** Test execution recipe for QA and devs adding regression coverage for the current purple-envelope edit/report workflow. | |||||
| **Date:** 2026-03-17 | |||||
| **Author:** Daniel Covington | |||||
| **Status:** Draft | |||||
| **Project:** workspace | |||||
| **Related:** See `test-design-architecture.md` for architectural blockers and testability gaps. | |||||
| --- | |||||
| ## Executive Summary | |||||
| **Scope:** Preserve the current behavior of the purple-envelope edit screen, including report content, mixed-format precinct ordering, election-date rendering, color assignment, empty states, and print-only markup boundaries. | |||||
| **Risk Summary:** | |||||
| - Total Risks: 7 (3 high-priority, 4 medium) | |||||
| - Critical Categories: DATA, TECH, BUS | |||||
| **Coverage Summary:** | |||||
| - P0 tests: ~4 | |||||
| - P1 tests: ~6 | |||||
| - P2 tests: ~4 | |||||
| - P3 tests: ~2 manual checks | |||||
| - **Total:** ~16 targeted checks plus 2 manual browser-print checks (~1-1.5 QA weeks) | |||||
| --- | |||||
| ## Not in Scope | |||||
| | Item | Reasoning | Mitigation | | |||||
| | --- | --- | --- | | |||||
| | **ReportMan/PDF exports** | Not part of this feature slice; separate export path and COM dependencies | Covered by existing manual export verification | | |||||
| | **Browser-generated print headers/footers** | Controlled by browser settings, not reliably by page code | Manual print smoke checklist documents expected operator setup | | |||||
| | **Jurisdiction import or unrelated kit workflows** | Outside the current purple-envelope regression target | Existing module-level validation remains separate | | |||||
| --- | |||||
| ## Dependencies & Test Blockers | |||||
| ### Backend/Architecture Dependencies (Pre-Implementation) | |||||
| 1. **Disposable test MDB strategy** - Dev - Pre-implementation | |||||
| - QA needs a resettable database copy or equivalent seed/reset routine. | |||||
| - Without this, controller/repository tests will be brittle and non-repeatable. | |||||
| 2. **Seed dataset for mixed-format precincts** - Dev + QA - Pre-implementation | |||||
| - QA needs representative data for `000x`, numeric, alpha-suffixed, alpha-only, and empty-state precinct cases. | |||||
| - Without this, ordering and aggregation checks will not cover the risky paths. | |||||
| 3. **Controller render harness convention** - Dev - Pre-implementation | |||||
| - QA needs a repeatable way to exercise `SwitchBoardPurpleEnvelopeEdit` and inspect the HTML body under IIS/ASPUnit-compatible flow. | |||||
| - Without this, only repository tests will be automated and the view contract stays manual. | |||||
| ### QA Infrastructure Setup (Pre-Implementation) | |||||
| 1. **ASPUnit extension points** | |||||
| - Add one focused test container for purple-envelope regression checks. | |||||
| - Keep tests deterministic and data-seeded. | |||||
| 2. **Test data fixtures** | |||||
| - Fixture set A: ballot ranges with known low/high numbers. | |||||
| - Fixture set B: mixed-format precinct ordering. | |||||
| - Fixture set C: missing/invalid `Electiondate`. | |||||
| 3. **Windows/IIS smoke environment** | |||||
| - One browser-print smoke pass for report-only printing and repeated-page top spacing. | |||||
| --- | |||||
| ## Risk Assessment | |||||
| ### High-Priority Risks (Score >=6) | |||||
| | Risk ID | Category | Description | Score | QA Test Coverage | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | **R-001** | DATA | Mixed-format precinct ordering changes | **6** | Seeded render assertions for both color table and report table | | |||||
| | **R-002** | DATA | Wrong low/high ballot aggregation per precinct | **6** | Repository aggregation tests plus rendered-value checks | | |||||
| | **R-006** | TECH | No isolated fixture/reset path for stable tests | **6** | Test harness readiness criteria and pre-test reset validation | | |||||
| ### Medium/Low-Priority Risks | |||||
| | Risk ID | Category | Description | Score | QA Test Coverage | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | R-003 | BUS | Election date renders incorrectly or crashes on bad input | 4 | Valid/missing/invalid setting render checks | | |||||
| | R-004 | OPS | Print-only scope or top spacing regresses | 4 | HTML/CSS assertions plus manual print smoke | | |||||
| | R-005 | BUS | Color-assignment actions regress while report tests are added | 4 | POST regression checks for kit-wide and precinct-specific updates | | |||||
| | R-007 | BUS | Empty precinct state breaks current report contract | 4 | Explicit no-data render check | | |||||
| --- | |||||
| ## Entry Criteria | |||||
| - [ ] Disposable test MDB or equivalent reset strategy available | |||||
| - [ ] Mixed-format precinct fixtures agreed and documented | |||||
| - [ ] IIS-accessible test path for controller/render verification available | |||||
| - [ ] Existing `Tests/Test_All.asp` runner still executes successfully | |||||
| - [ ] Manual Windows browser available for final print smoke | |||||
| ## Exit Criteria | |||||
| - [ ] All P0 checks passing | |||||
| - [ ] All P1 checks passing or formally triaged | |||||
| - [ ] No open high-risk regression on ordering, aggregation, or data header behavior | |||||
| - [ ] HTML render contract validated for empty state and report-only print container | |||||
| - [ ] Manual print smoke completed for report-only output and repeated-page spacing | |||||
| --- | |||||
| ## Test Coverage Plan | |||||
| **Note:** P0/P1/P2/P3 represent priority and risk, not execution timing. | |||||
| ### P0 (Critical) | |||||
| **Criteria:** Preserves operator-facing data correctness and high-risk regression paths with no acceptable workaround. | |||||
| | Test ID | Requirement | Test Level | Risk Link | Notes | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | **P0-001** | Report shows correct jurisdiction/JCode and formatted election label for seeded valid data | Integration | R-003 | Assert rendered HTML content | | |||||
| | **P0-002** | Mixed-format precincts render in the approved ascending order | Integration | R-001 | Cover `000x`, numeric, alpha-suffixed, alpha-only | | |||||
| | **P0-003** | Each precinct shows correct low/high ballot numbers from seeded records | Integration | R-002 | Assert repository output and rendered HTML | | |||||
| | **P0-004** | Empty precinct dataset renders current no-data message instead of broken markup | Integration | R-007 | Protect empty-state contract | | |||||
| **Total P0:** ~4 tests | |||||
| --- | |||||
| ### P1 (High) | |||||
| **Criteria:** Preserves important same-screen workflows and print-related regressions with moderate-to-high operational impact. | |||||
| | Test ID | Requirement | Test Level | Risk Link | Notes | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | **P1-001** | `AssignKitColorPost` updates all `InkjetRecords.ColorId` rows for the kit | Integration | R-005 | Seeded DB verification | | |||||
| | **P1-002** | `AssignPrecinctColorsPost` updates only the targeted precinct rows | Integration | R-005 | Ensure no cross-precinct bleed | | |||||
| | **P1-003** | Missing `Electiondate` does not error and leaves report date blank | Integration | R-003 | Assert safe fallback | | |||||
| | **P1-004** | Invalid `Electiondate` value preserves non-crashing fallback behavior | Integration | R-003 | Use non-date string fixture | | |||||
| | **P1-005** | Print container isolates the report section in markup/CSS | Integration | R-004 | Assert presence of `#purple-envelope-report-print` rules | | |||||
| | **P1-006** | `Ready To Assign STIDS` status still gates the existing form block | Integration | R-005 | Regression for same-screen conditional behavior | | |||||
| **Total P1:** ~6 tests | |||||
| --- | |||||
| ### P2 (Medium) | |||||
| **Criteria:** Lower-risk formatting and regression checks that still help freeze today’s behavior. | |||||
| | Test ID | Requirement | Test Level | Risk Link | Notes | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | **P2-001** | Full-year format renders as `Mon-YYYY` for valid election dates | Integration | R-003 | Example `May-2026` | | |||||
| | **P2-002** | Print spacer row and top buffer CSS remain present | Integration | R-004 | Markup/CSS contract only | | |||||
| | **P2-003** | Report font-size contract remains at current reduced print size | Integration | R-004 | CSS assertion | | |||||
| | **P2-004** | Color table and report table stay aligned on precinct order | Integration | R-001 | Prevent divergent ordering paths | | |||||
| **Total P2:** ~4 tests | |||||
| --- | |||||
| ### P3 (Low) | |||||
| **Criteria:** Manual, browser-dependent, or release-smoke-only validation. | |||||
| | Test ID | Requirement | Test Level | Notes | | |||||
| | --- | --- | --- | --- | | |||||
| | **P3-001** | Browser print preview shows only the report section | Manual Browser | Requires operator print settings | | |||||
| | **P3-002** | Page 2+ maintains visible top breathing room in print preview | Manual Browser | Validate on Windows/IIS browser | | |||||
| **Total P3:** ~2 checks | |||||
| --- | |||||
| ## Execution Strategy | |||||
| ### Every PR: ASPUnit + seeded render/repository checks (~5-10 min) | |||||
| - Run the existing `Tests/Test_All.asp` suite | |||||
| - Run the new purple-envelope regression container against disposable seeded data | |||||
| - Keep automated checks focused on repository correctness and rendered HTML contract | |||||
| ### Nightly: expanded seeded regression pass (~10-20 min) | |||||
| - Re-run the purple-envelope suite against larger mixed-format datasets | |||||
| - Include any fixture-reset verification and broader same-screen regression checks | |||||
| ### Weekly or Pre-Release: manual Windows/IIS print smoke (~15-30 min) | |||||
| - Confirm report-only printing | |||||
| - Confirm repeated-page top spacing | |||||
| - Confirm browser-dependent behavior still matches operator expectations | |||||
| --- | |||||
| ## QA Effort Estimate | |||||
| | Priority | Count | Effort Range | Notes | | |||||
| | --- | --- | --- | --- | | |||||
| | P0 | ~4 | ~8-12 hours | Core seeded render/aggregation checks | | |||||
| | P1 | ~6 | ~10-16 hours | POST regressions, fallbacks, print markup | | |||||
| | P2 | ~4 | ~6-10 hours | Formatting and alignment checks | | |||||
| | P3 | ~2 | ~2-4 hours | Manual print verification | | |||||
| | **Total** | ~16 | **~26-42 hours** | **~1-1.5 QA weeks** | | |||||
| **Assumptions:** | |||||
| - Disposable fixture/reset path is provided early. | |||||
| - Tests reuse existing ASPUnit conventions instead of introducing a new framework. | |||||
| - Manual print validation remains intentionally small and release-focused. | |||||
| --- | |||||
| ## Implementation Planning Handoff | |||||
| | Work Item | Owner | Target Milestone (Optional) | Dependencies/Notes | | |||||
| | --- | --- | --- | --- | | |||||
| | Add disposable MDB copy/reset process for purple-envelope tests | Dev | Sprint next | Required before automation | | |||||
| | Add repository regression tests for ballot range aggregation | Dev/QA | Sprint next | Uses fixture set A | | |||||
| | Add controller/render assertions for report HTML and sort order | Dev/QA | Sprint next | Uses fixture sets A/B/C | | |||||
| | Add manual print smoke checklist to release flow | QA | Pre-release | Browser-dependent only | | |||||
| --- | |||||
| ## Tooling & Access | |||||
| | Tool or Service | Purpose | Access Required | Status | | |||||
| | --- | --- | --- | --- | | |||||
| | IIS-hosted local app | Execute controller/render tests | Windows + IIS site access | Ready | | |||||
| | Disposable MDB copy | Stable seeded data | File-system access to test DB copy | Pending | | |||||
| | ASPUnit runner | Automated regression execution | Existing `Tests/Test_All.asp` access | Ready | | |||||
| | Windows browser print preview | Manual print smoke | Local browser access | Ready | | |||||
| **Access requests needed (if any):** | |||||
| - [ ] Disposable test database path and reset ownership confirmed | |||||
| --- | |||||
| ## Interworking & Regression | |||||
| | Service/Component | Impact | Regression Scope | Validation Steps | | |||||
| | --- | --- | --- | --- | | |||||
| | **KitController** | Orchestrates page model and POST actions | Existing edit page behavior plus report additions | Render and POST regression checks | | |||||
| | **InkjetRecordsRepository** | Supplies precinct lists, aggregation, and color updates | Ordering-sensitive and data-sensitive queries | Seeded repository assertions | | |||||
| | **SettingsRepository** | Supplies `Electiondate` | Header formatting/fallback | Valid/missing/invalid setting cases | | |||||
| | **SwitchBoardPurpleEnvelopeEdit.asp** | Final HTML/print contract | Report-only container, table content, empty state | HTML response assertions + manual print smoke | | |||||
| **Regression test strategy:** | |||||
| - Existing `Tests/Test_All.asp` must still pass. | |||||
| - New seeded regression checks should run before release on any change touching the purple-envelope screen, repository, or print CSS. | |||||
| - Manual Windows print smoke remains required for print-affecting changes. | |||||
| --- | |||||
| ## Appendix A: Code Examples & Tagging | |||||
| For this repo, prefer ASPUnit naming aligned to the existing suite rather than Playwright tagging: | |||||
| - `PurpleEnvelopeReport_Should_Render_ElectionDate` | |||||
| - `PurpleEnvelopeReport_Should_Sort_MixedPrecincts_Ascending` | |||||
| - `PurpleEnvelopeReport_Should_Show_BallotRanges_PerPrecinct` | |||||
| - `AssignPrecinctColors_Should_Update_Only_TargetPrecinct` | |||||
| Keep tests deterministic: | |||||
| - Seed explicit fixture data | |||||
| - Assert HTML or repository outputs directly | |||||
| - Avoid browser timing waits in automated checks | |||||
| --- | |||||
| ## Appendix B: Knowledge Base References | |||||
| - `risk-governance.md` | |||||
| - `probability-impact.md` | |||||
| - `test-levels-framework.md` | |||||
| - `test-priorities-matrix.md` | |||||
| - `test-quality.md` | |||||
| --- | |||||
| **Generated by:** BMad TEA Agent | |||||
| **Workflow:** `_bmad/tea/testarch/bmad-testarch-test-design` | |||||
| **Mode:** System-level (brownfield feature scope) | |||||
| @@ -0,0 +1,85 @@ | |||||
| --- | |||||
| title: TEA Test Design -> BMAD Handoff Document | |||||
| version: "1.0" | |||||
| workflowType: testarch-test-design-handoff | |||||
| inputDocuments: | |||||
| - D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md | |||||
| - D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md | |||||
| sourceWorkflow: testarch-test-design | |||||
| generatedBy: TEA Master Test Architect | |||||
| generatedAt: "2026-03-17T00:00:00" | |||||
| projectName: workspace | |||||
| --- | |||||
| # TEA -> BMAD Integration Handoff | |||||
| ## Purpose | |||||
| This document bridges the purple-envelope regression test design with future BMAD epic/story work so the current behavior contract is preserved during implementation. | |||||
| ## TEA Artifacts Inventory | |||||
| | Artifact | Path | BMAD Integration Point | | |||||
| | --- | --- | --- | | |||||
| | Test Design Architecture | `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md` | Architectural blockers, risk mitigation, testability requirements | | |||||
| | Test Design QA | `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md` | Story acceptance criteria and regression scenario coverage | | |||||
| | Risk Assessment | embedded in both docs | Epic quality gates and story priority | | |||||
| ## Epic-Level Integration Guidance | |||||
| ### Risk References | |||||
| - **R-001 / R-002** must be treated as epic-level quality blockers because ordering or aggregation defects change operator-facing output. | |||||
| - **R-006** must be scheduled as enabling work before expecting reliable automated regression coverage. | |||||
| ### Quality Gates | |||||
| - No story touching the purple-envelope edit page is complete until mixed-format precinct ordering is proven against seeded data. | |||||
| - No story touching the report query/view is complete until ballot low/high aggregation is verified. | |||||
| - Print-affecting changes require a manual Windows/IIS print smoke before release. | |||||
| ## Story-Level Integration Guidance | |||||
| ### P0/P1 Test Scenarios -> Story Acceptance Criteria | |||||
| - The purple-envelope report renders jurisdiction, JCode, and election date exactly as currently approved. | |||||
| - Precincts sort in the frozen ascending order for zero-padded, numeric, and alpha-suffixed values. | |||||
| - Each precinct shows the correct low/high ballot number range. | |||||
| - Kit-wide and precinct-specific color updates still work on the same screen. | |||||
| - Empty precinct datasets show the current no-data row without error. | |||||
| ### Data-TestId Requirements | |||||
| - Not applicable for Classic ASP in the current implementation. | |||||
| - Prefer stable HTML markers, predictable headings, and table structure for server-render assertions. | |||||
| ## Risk-to-Story Mapping | |||||
| | Risk ID | Category | P×I | Recommended Story/Epic | Test Level | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | R-001 | DATA | 2x3 | Purple-envelope report ordering contract | Integration | | |||||
| | R-002 | DATA | 2x3 | Purple-envelope ballot aggregation contract | Integration | | |||||
| | R-003 | BUS | 2x2 | Election date render/fallback behavior | Integration | | |||||
| | R-004 | OPS | 2x2 | Print-only report markup and manual print smoke | Integration + Manual | | |||||
| | R-005 | BUS | 2x2 | Preserve color-assignment workflows on same screen | Integration | | |||||
| | R-006 | TECH | 3x2 | Disposable MDB/reset harness for regression suite | Enabler | | |||||
| | R-007 | BUS | 2x2 | Empty-state render contract | Integration | | |||||
| ## Recommended BMAD -> TEA Workflow Sequence | |||||
| 1. TEA Test Design (`TD`) -> complete | |||||
| 2. BMAD Create Epics & Stories -> include regression-preservation stories and test-harness enabler work | |||||
| 3. TEA ATDD (`AT`) -> generate failing checks for P0 report-ordering and aggregation scenarios | |||||
| 4. BMAD Implementation -> add fixtures/tests/harness work | |||||
| 5. TEA Automate (`TA`) -> expand the suite beyond the initial P0/P1 checks | |||||
| 6. TEA Trace (`TR`) -> verify risk coverage completeness | |||||
| ## Phase Transition Quality Gates | |||||
| | From Phase | To Phase | Gate Criteria | | |||||
| | --- | --- | --- | | |||||
| | Test Design | Epic/Story Creation | R-001, R-002, and R-006 mitigation work represented in planning | | |||||
| | Epic/Story Creation | ATDD | P0 scenarios for ordering and aggregation translated into executable checks | | |||||
| | ATDD | Implementation | Seed fixtures and disposable DB approach agreed | | |||||
| | Implementation | Test Automation | Automated P0 checks passing | | |||||
| | Test Automation | Release | Manual print smoke complete for print-affecting changes | | |||||
Powered by TurnKey Linux.