@@ -16,6 +16,7 @@ extension type _NodeFS(JSObject _) implements JSObject {
1616 external void writeFileSync (JSString path, JSString data);
1717 external void mkdirSync (JSString path, JSObject ? options);
1818 external bool existsSync (JSString path);
19+ external JSString readFileSync (JSString path, [JSString ? encoding]);
1920}
2021
2122/// Extension type for Node.js path module
@@ -92,6 +93,9 @@ String getCoverageJson() {
9293}
9394
9495/// Write coverage data to a file (Node.js only)
96+ ///
97+ /// CRITICAL: Merges with existing coverage data instead of overwriting.
98+ /// Multiple test files call this function and we accumulate coverage.
9599void writeCoverageFile (String outputPath) {
96100 // Load Node.js modules
97101 final fsResult = _global.require.callAsFunction (null , 'fs' .toJS);
@@ -105,7 +109,6 @@ void writeCoverageFile(String outputPath) {
105109 }
106110
107111 // Type-checked casts: safe after isA<JSObject>() verification
108- // Extension type constructors require JSObject, casts are required for js_interop
109112 final fs = _NodeFS (fsResult as JSObject );
110113 final pathMod = _NodePath (pathResult as JSObject );
111114
@@ -116,7 +119,69 @@ void writeCoverageFile(String outputPath) {
116119 fs.mkdirSync (dir, options);
117120 }
118121
119- // Write coverage data
122+ // Get current coverage data
123+ final currentData = _getCoverageData ();
124+
125+ // If file exists, read and merge
126+ if (fs.existsSync (outputPath.toJS)) {
127+ try {
128+ final existing = fs.readFileSync (outputPath.toJS, 'utf8' .toJS);
129+ final json = (globalContext as JSObject )['JSON' ] as JSObject ;
130+ final parse = json['parse' ] as JSFunction ;
131+ final existingData = parse.callAsFunction (json, existing) as JSObject ;
132+
133+ // Merge existing into current
134+ _mergeData (currentData, existingData);
135+ } catch (_) {
136+ // Corrupt/empty file - ignore and overwrite
137+ }
138+ }
139+
140+ // Write merged data
120141 final jsonData = getCoverageJson ();
121142 fs.writeFileSync (outputPath.toJS, jsonData.toJS);
122143}
144+
145+ /// Merge existing coverage data into current data
146+ void _mergeData (JSObject current, JSObject existing) {
147+ final objClass = (globalContext as JSObject )['Object' ] as JSObject ;
148+ final keys = objClass['keys' ] as JSFunction ;
149+ final fileKeys = keys.callAsFunction (objClass, existing) as JSArray ;
150+
151+ final fileCount = (fileKeys.getProperty ('length' .toJS) as JSNumber ).toDartInt;
152+ for (var i = 0 ; i < fileCount; i++ ) {
153+ final fileKeyRaw = fileKeys.getProperty (i.toJS);
154+ if (fileKeyRaw == null ) continue ;
155+ final fileKey = fileKeyRaw as JSString ;
156+ final existingFileCov = existing.getProperty (fileKey) as JSObject ;
157+
158+ // Get or create file coverage
159+ final hasFile = (current.hasProperty (fileKey) as JSBoolean ).toDart;
160+ final currentFileCov = hasFile
161+ ? current.getProperty (fileKey) as JSObject
162+ : JSObject ();
163+
164+ if (! hasFile) {
165+ current.setProperty (fileKey, currentFileCov);
166+ }
167+
168+ // Merge line counts
169+ final lineKeys = keys.callAsFunction (objClass, existingFileCov) as JSArray ;
170+ final lineCount = (lineKeys.getProperty ('length' .toJS) as JSNumber ).toDartInt;
171+
172+ for (var j = 0 ; j < lineCount; j++ ) {
173+ final lineKeyRaw = lineKeys.getProperty (j.toJS);
174+ if (lineKeyRaw == null ) continue ;
175+ final lineKey = lineKeyRaw;
176+ final existingCount = existingFileCov.getProperty (lineKey) as JSNumber ;
177+ final hasLine = (currentFileCov.hasProperty (lineKey) as JSBoolean ).toDart;
178+ final currentCount = hasLine
179+ ? (currentFileCov.getProperty (lineKey) as JSNumber ).toDartDouble
180+ : 0.0 ;
181+
182+ // Add counts together
183+ final merged = currentCount + existingCount.toDartDouble;
184+ currentFileCov.setProperty (lineKey, merged.toJS);
185+ }
186+ }
187+ }
0 commit comments