diff --git a/.github/workflows/build_devtool.yml b/.github/workflows/build_devtool.yml index 6263bb692..f6c7ada43 100644 --- a/.github/workflows/build_devtool.yml +++ b/.github/workflows/build_devtool.yml @@ -22,7 +22,7 @@ jobs: uses: flutter-actions/setup-flutter@v2 with: channel: stable - version: 3.29.0 + version: 3.44.0 - name: Run Flutter Test run: tool/gh_actions/devtool/run_devtool_test.sh diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index f2ed2cf65..938afc975 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -34,6 +34,8 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@v1 + with: + sdk: 3.13.0 - name: Setup Node uses: actions/setup-node@v4 @@ -75,7 +77,9 @@ jobs: deploy-documentation: name: Deploy Documentation - needs: run-checks + needs: + - run-checks + - build-devtool if: github.event_name == 'push' permissions: contents: write # required for "JamesIves/github-pages-deploy-action" @@ -94,6 +98,12 @@ jobs: - name: Generate project documentation run: tool/gh_actions/generate_documentation.sh + - name: Add standalone DevTools application + uses: actions/download-artifact@v4 + with: + name: rohd-devtools-standalone + path: doc/api/rohd_devtools_extension + - name: Deploy generated documentation uses: JamesIves/github-pages-deploy-action@v4 with: @@ -111,7 +121,7 @@ jobs: uses: flutter-actions/setup-flutter@v2 with: channel: stable - version: 3.29.0 + version: 3.44.0 - name: Run Flutter Test run: tool/gh_actions/devtool/run_devtool_test.sh @@ -122,3 +132,13 @@ jobs: - name: Test DevTools Installation run: tool/gh_actions/devtool/test_devtools_install.sh extension/devtools + - name: Build standalone DevTools application + run: rohd_devtools_extension/tool/gh_actions/build_app.sh /rohd_devtools_extension/ + + - name: Upload standalone DevTools application + uses: actions/upload-artifact@v4 + with: + name: rohd-devtools-standalone + path: rohd_devtools_extension/build/web + if-no-files-found: error + include-hidden-files: true diff --git a/analysis_options.yaml b/analysis_options.yaml index b4e2c5480..2399314f3 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,6 +12,13 @@ analyzer: - packages/rohd_waveform - rohd_devtools_extension - rohd_extension + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** # keep up to date, matching https://dart.dev/tools/linter-rules/all # some lints are not yet available, so disabled and marked with [not currently recognized] diff --git a/rohd_devtools_extension/Makefile b/rohd_devtools_extension/Makefile index 19c7ac8ca..c8915711c 100644 --- a/rohd_devtools_extension/Makefile +++ b/rohd_devtools_extension/Makefile @@ -1,8 +1,9 @@ # Minimal build entrypoints for the upstream standalone extension package. ROOT := $(shell pwd) +FLUTTER_WEB_BUILD_ARGS ?= -.PHONY: all linux clean-linux help +.PHONY: all linux web web-release clean-linux clean-web help all: linux @@ -10,14 +11,28 @@ linux: @flutter pub get @flutter build linux --target=lib/main_standalone.dart +web: + @flutter pub get + @flutter build web --release --target=lib/main_standalone.dart $(FLUTTER_WEB_BUILD_ARGS) + +web-release: + @rm -f build/web/index.html + @$(MAKE) web + clean-linux: -rm -rf $(ROOT)/build/linux -rm -rf $(ROOT)/linux/flutter/ephemeral +clean-web: + -rm -rf $(ROOT)/build/web + help: @echo "ROHD DevTools Extension" @echo "" @echo "Targets:" @echo " all - Build the Linux standalone app (default)" @echo " linux - Run flutter pub get and build Linux standalone" - @echo " clean-linux - Remove Linux build outputs" \ No newline at end of file + @echo " web - Build the web standalone app" + @echo " web-release - Force a release web build" + @echo " clean-linux - Remove Linux build outputs" + @echo " clean-web - Remove web build outputs" \ No newline at end of file diff --git a/rohd_devtools_extension/README.md b/rohd_devtools_extension/README.md index acb09c486..b78feb2ab 100644 --- a/rohd_devtools_extension/README.md +++ b/rohd_devtools_extension/README.md @@ -37,6 +37,19 @@ cd rohd_devtools_extension flutter run --release -d web-server --web-port=9099 --web-hostname=0.0.0.0 lib/main_standalone.dart ``` +Build a static bundle for a repository-scoped web path: + +```sh +cd rohd_devtools_extension +tool/gh_actions/build_app.sh /rohd_devtools_extension/ +``` + +The bundle is written to `build/web/`. The optional argument must start and end +with `/`; it becomes the Flutter base href used when the app is hosted below a +site root. Local builds default to `/rohd_devtools_extension/`. On pushes to +`main`, the `General` workflow includes this bundle in the existing ROHD +documentation deployment at `/rohd_devtools_extension/`. + Run the release Linux standalone form: ```sh diff --git a/rohd_devtools_extension/analysis_options.yaml b/rohd_devtools_extension/analysis_options.yaml index f82d6cc51..1bfb3d100 100644 --- a/rohd_devtools_extension/analysis_options.yaml +++ b/rohd_devtools_extension/analysis_options.yaml @@ -2,6 +2,14 @@ # https://rydmike.com/blog_flutter_linting.html analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** language: strict-casts: true strict-inference: true diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/analysis_options.yaml b/rohd_devtools_extension/packages/rohd_devtools_widgets/analysis_options.yaml index 572dd239d..a96029588 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/analysis_options.yaml +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/analysis_options.yaml @@ -1 +1,10 @@ -include: package:lints/recommended.yaml +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** +include: ../../analysis_options.yaml diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/assets/extension/module_info_helper.js b/rohd_devtools_extension/packages/rohd_devtools_widgets/assets/extension/module_info_helper.js new file mode 100644 index 000000000..fdcf44069 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/assets/extension/module_info_helper.js @@ -0,0 +1,159 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_info_helper.js +// Shared helper for querying FLC module info via the rohd_extension commands. +// +// Used by both rohd-schematic-viewer (plain JS) and rohd-wave-viewer +// (TypeScript) VS Code extensions to resolve format availability for a module. +// +// 2026 May +// Author: Desmond Kirkpatrick + +const vscode = require('vscode'); +const path = require('path'); +const fs = require('fs'); + +/** + * Resolve the `.flc.json` sidecar path for a given document URI. + * Delegates to the `rohd.resolveFlcPath` command in rohd_extension. + * Falls back to local convention if the command is unavailable. + * + * @param {import('vscode').Uri} documentUri + * @param {import('vscode').OutputChannel} [output] + * @returns {Promise} + */ +async function resolveFlcPath(documentUri, output) { + try { + const result = await vscode.commands.executeCommand('rohd.resolveFlcPath', { + documentFsPath: documentUri.fsPath, + }); + return result ?? null; + } catch (_) { + // rohd_extension not installed - fall back to local convention. + const fsPath = documentUri.fsPath; + const dir = path.dirname(fsPath); + const base = path.basename(fsPath); + // .vcd/.fst/.ghw/.rohd.json -> .flc.json + const flcName = base.replace(/\.(vcd|fst|ghw|rohd\.json)$/i, '.flc.json'); + if (flcName === base) return null; + const flcPath = path.join(dir, flcName); + return fs.existsSync(flcPath) ? flcPath : null; + } +} + +/** + * Build a RohdModuleInfo-compatible JSON object for [moduleName]. + * + * Delegates to the `rohd.queryModule` VS Code command provided by + * rohd_extension, which owns all FLC parsing logic. + * + * @param {import('vscode').Uri} documentUri + * @param {string|null} moduleName + * @param {string[]|undefined} instancePath + * @param {import('vscode').OutputChannel} [output] + * @returns {Promise} matching RohdModuleInfo.toJson() schema + */ +async function buildModuleInfo(documentUri, moduleName, instancePath, output) { + const flcPath = await resolveFlcPath(documentUri, output); + if (!flcPath) { + return { + extensionAvailable: true, + module: moduleName, + formats: {}, + error: 'No .flc.json sidecar found. Generate with TraceService.writeFlcFiles().', + fstLoading: false, + }; + } + + const queryModule = async module => { + const payload = { + flcPath, + module, + }; + if (instancePath && instancePath.length > 0) { + payload.instancePath = instancePath; + } + return vscode.commands.executeCommand('rohd.queryModule', payload); + }; + + try { + let info = await queryModule(moduleName); + + const hasFormats = + info && typeof info === 'object' && Object.keys(info.formats ?? {}).length > 0; + if (!hasFormats && instancePath && instancePath.length > 0) { + const instanceName = [...instancePath] + .reverse() + .find(segment => typeof segment === 'string' && segment.length > 0); + if (instanceName && instanceName !== moduleName) { + if (output) { + output.appendLine( + '[moduleInfo] no formats for ' + + String(moduleName ?? '') + + '; retrying with instance name ' + + instanceName, + ); + } + const fallbackInfo = await queryModule(instanceName); + const fallbackHasFormats = + fallbackInfo && + typeof fallbackInfo === 'object' && + Object.keys(fallbackInfo.formats ?? {}).length > 0; + if (fallbackHasFormats) { + info = fallbackInfo; + } + } + } + + return info ?? { + extensionAvailable: true, + module: moduleName, + formats: {}, + error: 'rohd.queryModule returned no result.', + fstLoading: false, + }; + } catch (e) { + if (output) output.appendLine('[moduleInfo] rohd.queryModule failed: ' + e.message); + return { + extensionAvailable: true, + module: moduleName, + formats: {}, + error: 'Install the ROHD extension for source format detection.', + fstLoading: false, + }; + } +} + +/** + * Look up signal source frames via rohd_extension's `rohd.lookupSignal` command. + * + * @param {string} flcPath + * @param {string|null} moduleName + * @param {string} signalName + * @param {string|undefined} format + * @param {import('vscode').OutputChannel} [output] + * @returns {Promise} + */ +async function lookupSignalFrames(flcPath, moduleName, signalName, format, output) { + try { + const frames = await vscode.commands.executeCommand('rohd.lookupSignal', { + flcPath, + module: moduleName, + signal: signalName, + format, + }); + const typedFrames = frames ?? []; + const filteredFrames = format + ? typedFrames.filter(frame => (frame?.type ?? 'rohd') === format) + : typedFrames; + // Reverse to outermost-first order (the ROHD extension returns + // innermost-first, matching raw stack-trace order). + return filteredFrames.reverse(); + } catch (e) { + if (output) output.appendLine('[crossProbe] rohd.lookupSignal failed: ' + e.message); + return []; + } +} + +module.exports = { resolveFlcPath, buildModuleInfo, lookupSignalFrames }; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart index ca31b9ace..799f4e945 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart @@ -8,37 +8,27 @@ // 2026 April // Author: Desmond Kirkpatrick -// Help -export 'src/markdown_help_button.dart'; - // Overlay export 'src/app_bar_overlay.dart'; - +// Shared "Expand Bits" / "Define Bit Fields" popup-menu helpers +export 'src/bit_expansion_menu.dart'; +// Bit-field parsing, formatting, and dialog utilities +export 'src/bit_field_utils.dart'; // PNG export export 'src/capture_boundary.dart'; -export 'src/export_button.dart'; -export 'src/export_toast.dart'; -export 'src/save_png_stub.dart' - if (dart.library.io) 'src/save_png_native.dart' - if (dart.library.js_interop) 'src/save_png_web.dart'; - -// Cross-probing -export 'src/cross_probe_service.dart'; export 'src/cross_probe_button.dart'; export 'src/cross_probe_menu.dart'; - +// Cross-probing +export 'src/cross_probe_service.dart'; +export 'src/export_button.dart'; +export 'src/export_toast.dart'; // Logic type utilities export 'src/logic_type_utils.dart'; - -// Bit-field parsing, formatting, and dialog utilities -export 'src/bit_field_utils.dart'; - -// Shared "Expand Bits" / "Define Bit Fields" popup-menu helpers -export 'src/bit_expansion_menu.dart'; - -// Shared signal display-format preferences and value formatting -export 'src/signal_value_format_registry.dart'; - +// Help +export 'src/markdown_help_button.dart'; +export 'src/rohd_extension_client.dart'; // ROHD extension client export 'src/rohd_extension_status.dart'; -export 'src/rohd_extension_client.dart'; +export 'src/save_png_stub.dart' + if (dart.library.io) 'src/save_png_native.dart' + if (dart.library.js_interop) 'src/save_png_web.dart'; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/app_bar_overlay.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/app_bar_overlay.dart index ba210d5b2..7213f454c 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/app_bar_overlay.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/app_bar_overlay.dart @@ -14,6 +14,7 @@ // 2026 April // Author: Desmond Kirkpatrick +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Wraps a [body] widget and an [appBar] widget, where the AppBar @@ -41,10 +42,11 @@ class AppBarOverlay extends StatefulWidget { /// Duration of the slide animation. final Duration animationDuration; + /// Creates an app bar overlay around [body]. const AppBarOverlay({ - super.key, required this.appBar, required this.body, + super.key, this.autoHide = false, this.triggerHeight = 12, this.panelOpacity = 0.92, @@ -53,6 +55,18 @@ class AppBarOverlay extends StatefulWidget { @override State createState() => _AppBarOverlayState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('autoHide', autoHide)) + ..add(DoubleProperty('triggerHeight', triggerHeight)) + ..add(DoubleProperty('panelOpacity', panelOpacity)) + ..add( + DiagnosticsProperty('animationDuration', animationDuration), + ); + } } class _AppBarOverlayState extends State @@ -70,11 +84,13 @@ class _AppBarOverlayState extends State _slideAnimation = Tween( begin: const Offset(0, -1), // fully off-screen above end: Offset.zero, - ).animate(CurvedAnimation( - parent: _controller, - curve: Curves.easeOutCubic, - reverseCurve: Curves.easeInCubic, - )); + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + reverseCurve: Curves.easeInCubic, + ), + ); // If not auto-hiding, snap open. if (!widget.autoHide) { @@ -105,7 +121,9 @@ class _AppBarOverlayState extends State } void _hide() { - if (!widget.autoHide) return; + if (!widget.autoHide) { + return; + } _controller.reverse(); } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_expansion_menu.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_expansion_menu.dart index c01c55e31..ec72c54ed 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_expansion_menu.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_expansion_menu.dart @@ -10,21 +10,19 @@ // 2026 January // Author: Desmond Kirkpatrick -import 'package:flutter/material.dart'; - -import 'bit_field_utils.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:rohd_devtools_widgets/src/bit_field_utils.dart'; /// Popup-menu values used by the bit-expansion items. /// /// Callers may match against these string constants when handling a /// `showMenu` result that includes bit-expansion items. abstract final class BitExpansionMenuValues { - /// Menu item: "Expand Bits [N]" — expand each bit (or a chosen range) - /// as a synthesized 1-bit waveform. + /// "Expand Bits" menu item for expanding each bit (or a chosen range) as a + /// synthesized 1-bit waveform. static const String expandBits = 'expand_bits'; - /// Menu item: "Define Bit Fields [N]..." — open a dialog that lets the - /// user name arbitrary bit ranges. + /// "Define Bit Fields" menu item for naming arbitrary bit ranges. static const String defineFields = 'define_fields'; } @@ -44,6 +42,8 @@ class BitExpandRangeAction extends BitExpansionAction { /// High bit (inclusive) of the range to expand. final int bitEnd; + /// Creates an action to expand the inclusive range from [bitStart] to + /// [bitEnd]. const BitExpandRangeAction(this.bitStart, this.bitEnd); } @@ -53,6 +53,7 @@ class BitDefineFieldsAction extends BitExpansionAction { /// The user-defined bit fields. final List fields; + /// Creates an action defining [fields] for a signal. const BitDefineFieldsAction(this.fields); } @@ -73,24 +74,24 @@ List> buildBitExpansionMenuItems({ double fontSize = 13, double itemHeight = 32, bool includeDivider = false, -}) { - return >[ - if (includeDivider) const PopupMenuDivider(height: 8), - PopupMenuItem( - height: itemHeight, - value: BitExpansionMenuValues.expandBits, - child: Text('Expand Bits [$width]', style: TextStyle(fontSize: fontSize)), - ), - PopupMenuItem( - height: itemHeight, - value: BitExpansionMenuValues.defineFields, - child: Text( - 'Define Bit Fields [$width]...', - style: TextStyle(fontSize: fontSize), +}) => + >[ + if (includeDivider) const PopupMenuDivider(height: 8), + PopupMenuItem( + height: itemHeight, + value: BitExpansionMenuValues.expandBits, + child: + Text('Expand Bits [$width]', style: TextStyle(fontSize: fontSize)), ), - ), - ]; -} + PopupMenuItem( + height: itemHeight, + value: BitExpansionMenuValues.defineFields, + child: Text( + 'Define Bit Fields [$width]...', + style: TextStyle(fontSize: fontSize), + ), + ), + ]; /// Translate a popup-menu [value] returned by `showMenu` into a /// [BitExpansionAction], showing any follow-up dialog as needed. @@ -122,7 +123,9 @@ Future resolveBitExpansionMenuValue( signalName: signalName, width: width, ); - if (parsed == null) return null; + if (parsed == null) { + return null; + } final (high, low) = parsed; return BitExpandRangeAction(low, high); } @@ -132,7 +135,9 @@ Future resolveBitExpansionMenuValue( signalName: signalName, width: width, ); - if (fields == null || fields.isEmpty) return null; + if (fields == null || fields.isEmpty) { + return null; + } return BitDefineFieldsAction(fields); } return null; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_field_utils.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_field_utils.dart index efb61eb93..9b705cdfd 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_field_utils.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_field_utils.dart @@ -7,7 +7,7 @@ // 2025 May // Author: Desmond Kirkpatrick -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; /// A named bit-field definition within a bitvector signal. class BitFieldDef { @@ -20,6 +20,7 @@ class BitFieldDef { /// Low bit (inclusive, LSB of the field). final int low; + /// Creates a bit-field definition. const BitFieldDef({ required this.name, required this.high, @@ -50,16 +51,22 @@ abstract final class BitFieldUtils { static (int, int)? parseBitRange(String input, int maxBit) { if (input.contains(':')) { final parts = input.split(':'); - if (parts.length != 2) return null; + if (parts.length != 2) { + return null; + } final high = int.tryParse(parts[0].trim()); final low = int.tryParse(parts[1].trim()); - if (high == null || low == null) return null; + if (high == null || low == null) { + return null; + } final h = high.clamp(0, maxBit); final l = low.clamp(0, maxBit); return h >= l ? (h, l) : (l, h); } final bit = int.tryParse(input); - if (bit == null) return null; + if (bit == null) { + return null; + } final clamped = bit.clamp(0, maxBit); return (clamped, clamped); } @@ -76,7 +83,9 @@ abstract final class BitFieldUtils { final fields = []; for (final rawLine in lines) { final line = rawLine.trim(); - if (line.isEmpty) continue; + if (line.isEmpty) { + continue; + } final tokens = _splitAsciiWhitespace(line); // Try: name high:low @@ -158,7 +167,9 @@ abstract final class BitFieldUtils { } static bool _isWord(String value) { - if (value.isEmpty) return false; + if (value.isEmpty) { + return false; + } for (var i = 0; i < value.length; i++) { final char = value.codeUnitAt(i); final isUppercase = char >= 65 && char <= 90; @@ -173,10 +184,14 @@ abstract final class BitFieldUtils { } static bool _isUnsignedDecimal(String value) { - if (value.isEmpty) return false; + if (value.isEmpty) { + return false; + } for (var i = 0; i < value.length; i++) { final char = value.codeUnitAt(i); - if (char < 48 || char > 57) return false; + if (char < 48 || char > 57) { + return false; + } } return true; } @@ -208,37 +223,37 @@ Future<(int, int)?> showBitRangeDialog( final result = await showDialog( context: context, barrierColor: Colors.black26, - builder: (ctx) { - return AlertDialog( - title: Text( - '$signalName [$width bits]', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + builder: (ctx) => AlertDialog( + title: Text( + '$signalName [$width bits]', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + content: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration( + labelText: 'Bit range (high:low) or single bit', + hintText: '$maxBit:0', + isDense: true, ), - content: TextField( - controller: controller, - autofocus: true, - decoration: InputDecoration( - labelText: 'Bit range (high:low) or single bit', - hintText: '$maxBit:0', - isDense: true, - ), - onSubmitted: (value) => Navigator.of(ctx).pop(value), + onSubmitted: (value) => Navigator.of(ctx).pop(value), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(ctx).pop(controller.text), - child: const Text('OK'), - ), - ], - ); - }, + TextButton( + onPressed: () => Navigator.of(ctx).pop(controller.text), + child: const Text('OK'), + ), + ], + ), ); - if (result == null || result.trim().isEmpty) return null; + if (result == null || result.trim().isEmpty) { + return null; + } return BitFieldUtils.parseBitRange(result.trim(), maxBit); } @@ -257,12 +272,11 @@ Future?> showDefineBitFieldsDialog( // newline and place the cursor at the end so the user can immediately // type additional fields without accidentally replacing existing ones. final hasExisting = existingDefs != null && existingDefs.isNotEmpty; + String formatField(BitFieldDef field) => field.high == field.low + ? '${field.name} ${field.high}' + : '${field.name} ${field.high}:${field.low}'; final initialText = hasExisting - ? '${existingDefs.map((f) { - return f.high == f.low - ? '${f.name} ${f.high}' - : '${f.name} ${f.high}:${f.low}'; - }).join('\n')}\n' + ? '${existingDefs.map(formatField).join('\n')}\n' : 'field0 $maxBit:0'; final controller = TextEditingController(text: initialText); @@ -282,44 +296,44 @@ Future?> showDefineBitFieldsDialog( final result = await showDialog( context: context, barrierColor: Colors.black26, - builder: (ctx) { - return AlertDialog( - title: Text( - '$signalName [$width bits] — Define Fields', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), - ), - content: SizedBox( - width: 320, - child: TextField( - controller: controller, - autofocus: true, - maxLines: 8, - minLines: 3, - style: const TextStyle(fontFamily: 'monospace', fontSize: 13), - decoration: InputDecoration( - labelText: 'One field per line: name high:low', - hintText: 'exponent $maxBit:${maxBit - 10}\n' - 'mantissa ${maxBit - 11}:0', - isDense: true, - border: const OutlineInputBorder(), - ), + builder: (ctx) => AlertDialog( + title: Text( + '$signalName [$width bits] — Define Fields', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + content: SizedBox( + width: 320, + child: TextField( + controller: controller, + autofocus: true, + maxLines: 8, + minLines: 3, + style: const TextStyle(fontFamily: 'monospace', fontSize: 13), + decoration: InputDecoration( + labelText: 'One field per line: name high:low', + hintText: 'exponent $maxBit:${maxBit - 10}\n' + 'mantissa ${maxBit - 11}:0', + isDense: true, + border: const OutlineInputBorder(), ), ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(ctx).pop(controller.text), - child: const Text('OK'), - ), - ], - ); - }, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(controller.text), + child: const Text('OK'), + ), + ], + ), ); - if (result == null || result.trim().isEmpty) return null; + if (result == null || result.trim().isEmpty) { + return null; + } final fields = BitFieldUtils.parseBitFieldDefs(result, maxBit); return fields.isEmpty ? null : fields; } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_button.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_button.dart index ea6148492..6dc511088 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_button.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_button.dart @@ -7,8 +7,9 @@ // 2026 June // Author: Desmond Kirkpatrick +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'cross_probe_service.dart'; +import 'package:rohd_devtools_widgets/src/cross_probe_service.dart'; /// A toolbar icon button for cross-probing signal selections between viewers. /// @@ -26,23 +27,27 @@ class CrossProbeButton extends StatelessWidget { const CrossProbeButton({required this.service, super.key}); @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: service.isActive, - builder: (context, active, _) { - final color = active - ? Theme.of(context).colorScheme.primary - : Theme.of(context).disabledColor; - return Tooltip( - message: active - ? 'Cross-probing active — tap to disable' - : 'Cross-probing disabled — tap to enable', - child: IconButton( - icon: Icon(Icons.compare_arrows, color: color), - onPressed: () => service.isActive.value = !service.isActive.value, - ), - ); - }, - ); + Widget build(BuildContext context) => ValueListenableBuilder( + valueListenable: service.isActive, + builder: (context, active, _) { + final color = active + ? Theme.of(context).colorScheme.primary + : Theme.of(context).disabledColor; + return Tooltip( + message: active + ? 'Cross-probing active — tap to disable' + : 'Cross-probing disabled — tap to enable', + child: IconButton( + icon: Icon(Icons.compare_arrows, color: color), + onPressed: () => service.isActive.value = !service.isActive.value, + ), + ); + }, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('service', service)); } } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_menu.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_menu.dart index b8134e4ad..0c186214d 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_menu.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_menu.dart @@ -15,9 +15,9 @@ // 2026 June // Author: Desmond Kirkpatrick -import 'package:flutter/material.dart'; +import 'package:material_ui/material_ui.dart'; -import 'rohd_extension_status.dart'; +import 'package:rohd_devtools_widgets/src/rohd_extension_status.dart'; /// Returns the source formats currently navigable for the active module. /// @@ -30,10 +30,8 @@ typedef GoToSourceCallback = void Function( RohdSourceFormat format, List signalPaths); /// Builds an icon for a source/output [format]. -typedef SourceFormatIconBuilder = Widget Function( - RohdSourceFormat format, { - double size, -}); +typedef SourceFormatIconBuilder = Widget Function(RohdSourceFormat format, + {double size}); /// Prefix used to encode source-navigation entries in a `String`-valued popup /// menu (e.g. `'goto_source:rohd'`). Allows the shared items to coexist with @@ -139,7 +137,9 @@ Widget _sourceFormatAssetIcon( ), ); - if (!isDark) return image; + if (!isDark) { + return image; + } return Container( width: size + 4, @@ -189,11 +189,7 @@ PopupMenuItem buildRohdPopupMenuItem({ value: value, height: height, enabled: enabled, - child: sourcePopupMenuRow( - icon: icon, - label: label, - textStyle: textStyle, - ), + child: sourcePopupMenuRow(icon: icon, label: label, textStyle: textStyle), ); /// Compact strip of source/output format icons for trace-picker menu rows. diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_service.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_service.dart index 694950740..4bfe77cb2 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_service.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_service.dart @@ -64,7 +64,9 @@ class LocalCrossProbeChannel extends ChangeNotifier { /// /// Does nothing when [signalPaths] is empty. void broadcast(List signalPaths, String source) { - if (signalPaths.isEmpty) return; + if (signalPaths.isEmpty) { + return; + } _lastSource = source; _lastPaths = List.unmodifiable(signalPaths); notifyListeners(); @@ -75,7 +77,7 @@ class LocalCrossProbeChannel extends ChangeNotifier { /// /// Create one [LocalCrossProbeService] per viewer, all sharing the same /// [LocalCrossProbeChannel]. Each service filters out its own broadcasts -/// (matched by [source]) so viewers do not receive their own selections. +/// (matched by the source tag) so viewers do not receive their own selections. /// /// ```dart /// final channel = LocalCrossProbeChannel(); @@ -98,7 +100,7 @@ class LocalCrossProbeService implements CrossProbeService { /// Creates a [LocalCrossProbeService] backed by [channel]. /// - /// [source] is the identifier used to filter self-broadcasts. Use a + /// [source] identifies self-broadcasts to filter. Use a /// stable, descriptive tag such as `'waveform'` or `'schematic'`. LocalCrossProbeService( LocalCrossProbeChannel channel, { @@ -109,15 +111,21 @@ class LocalCrossProbeService implements CrossProbeService { } void _onChannelMessage() { - if (!isActive.value) return; + if (!isActive.value) { + return; + } final src = _channel.lastSource; - if (src == null || src == _source) return; // ignore own broadcasts + if (src == null || src == _source) { + return; + } incomingSignals.value = _channel.lastPaths; } @override void send(List signalPaths, {required String source}) { - if (!isActive.value || signalPaths.isEmpty) return; + if (!isActive.value || signalPaths.isEmpty) { + return; + } _channel.broadcast(signalPaths, source); } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_button.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_button.dart index 4c0dd1327..d48f2e6cf 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_button.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_button.dart @@ -7,6 +7,7 @@ // 2026 April // Author: Desmond Kirkpatrick +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Small camera-icon button for triggering PNG export. @@ -20,9 +21,10 @@ class ExportPngButton extends StatelessWidget { /// Tooltip text shown on hover. final String tooltip; + /// Creates a PNG export button. const ExportPngButton({ - super.key, required this.onPressed, + super.key, this.tooltip = 'Export as PNG', }); @@ -50,4 +52,12 @@ class ExportPngButton extends StatelessWidget { ), ); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(StringProperty('tooltip', tooltip)); + } } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/logic_type_utils.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/logic_type_utils.dart index adf89e928..d9602c5c3 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/logic_type_utils.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/logic_type_utils.dart @@ -271,9 +271,9 @@ String _extractContiguousBits(String binaryValue, int startBit, int width) { final totalWidth = binaryValue.length; final endBit = startBit + width; // exclusive if (startBit >= 0 && endBit <= totalWidth) { - return LogicValue.ofString(binaryValue) - .slice(endBit - 1, startBit) - .toString(includeWidth: false); + return LogicValue.ofString( + binaryValue, + ).slice(endBit - 1, startBit).toString(includeWidth: false); } final result = StringBuffer(); @@ -309,9 +309,9 @@ String? hexToBinary(String hexValue, int width) { final sourceWidth = cleaned.length * 4; final parseWidth = sourceWidth > width ? sourceWidth : width; try { - return LogicValue.ofRadixString("$parseWidth'h$cleaned") - .slice(width - 1, 0) - .toString(includeWidth: false); + return LogicValue.ofRadixString( + "$parseWidth'h$cleaned", + ).slice(width - 1, 0).toString(includeWidth: false); } on Exception { return null; } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/markdown_help_button.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/markdown_help_button.dart index ddc373afd..2fce2e0ba 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/markdown_help_button.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/markdown_help_button.dart @@ -20,6 +20,9 @@ // 2026 March // Author: Desmond Kirkpatrick +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// A help button that loads its content from a markdown asset file. @@ -109,6 +112,22 @@ class MarkdownHelpButton extends StatefulWidget { @override State createState() => _MarkdownHelpButtonState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('assetPath', assetPath)) + ..add(DiagnosticsProperty('isDark', isDark)) + ..add(StringProperty('label', label)) + ..add(StringProperty('package', package)) + ..add( + DiagnosticsProperty?>( + 'substitutions', + substitutions, + ), + ); + } } class _MarkdownHelpButtonState extends State { @@ -118,7 +137,7 @@ class _MarkdownHelpButtonState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - _loadContent(); + unawaited(_loadContent()); } @override @@ -126,7 +145,7 @@ class _MarkdownHelpButtonState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.assetPath != widget.assetPath || oldWidget.package != widget.package) { - _loadContent(); + unawaited(_loadContent()); } } @@ -142,8 +161,9 @@ class _MarkdownHelpButtonState extends State { // Use catch-all because AssetBundle.loadString throws FlutterError // (an Error, not Exception) when the asset is missing. try { - raw = await assetBundle - .loadString('packages/${widget.package}/${widget.assetPath}'); + raw = await assetBundle.loadString( + 'packages/${widget.package}/${widget.assetPath}', + ); // ignore: avoid_catches_without_on_clauses } catch (_) { raw = await assetBundle.loadString(widget.assetPath); @@ -188,9 +208,7 @@ class _MarkdownHelpButtonState extends State { decoration: BoxDecoration( color: isDark ? const Color(0xFF1E1E1E) : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isDark ? Colors.white24 : Colors.black12, - ), + border: Border.all(color: isDark ? Colors.white24 : Colors.black12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isDark ? 0.4 : 0.15), @@ -210,15 +228,21 @@ class _MarkdownHelpButtonState extends State { child: GestureDetector( onTap: () { if (_content != null) { - _showHelpDialog(context, _content!, - isDark: isDark, titleIcon: widget.titleIcon); + _showHelpDialog( + context, + _content!, + isDark: isDark, + titleIcon: widget.titleIcon, + ); } }, child: Padding( padding: const EdgeInsets.all(8), child: widget.labelIcon ?? - Text(widget.label, - style: const TextStyle(fontSize: 18, inherit: false)), + Text( + widget.label, + style: const TextStyle(fontSize: 18, inherit: false), + ), ), ), ), @@ -241,91 +265,111 @@ class _MarkdownHelpButtonState extends State { final widgets = []; for (final block in content.detailBlocks) { if (block is _HeadingBlock) { - widgets.add(Padding( - padding: const EdgeInsets.only(top: 16, bottom: 4), - child: Text(block.text, + widgets.add( + Padding( + padding: const EdgeInsets.only(top: 16, bottom: 4), + child: Text( + block.text, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: headingColor, - )), - )); + ), + ), + ), + ); } else if (block is _EntryBlock) { - widgets.add(Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: Text(block.key, + widgets.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: Text( + block.key, style: TextStyle( fontFamily: 'monospace', fontSize: 13, color: keyColor, - )), - ), - Expanded( - child: Text(block.description, - style: TextStyle(fontSize: 13, color: fgColor)), - ), - ], + ), + ), + ), + Expanded( + child: Text( + block.description, + style: TextStyle(fontSize: 13, color: fgColor), + ), + ), + ], + ), ), - )); + ); } else if (block is _ParagraphBlock) { - widgets.add(Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: - Text(block.text, style: TextStyle(fontSize: 13, color: fgColor)), - )); + widgets.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + block.text, + style: TextStyle(fontSize: 13, color: fgColor), + ), + ), + ); } } - showDialog( - context: context, - builder: (ctx) => Dialog( - backgroundColor: bgColor, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Title row - Row( - children: [ - if (titleIcon != null) ...[ - titleIcon, - const SizedBox(width: 10), - ], - Expanded( - child: Text(content.title, + unawaited( + showDialog( + context: context, + builder: (ctx) => Dialog( + backgroundColor: bgColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title row + Row( + children: [ + if (titleIcon != null) ...[ + titleIcon, + const SizedBox(width: 10), + ], + Expanded( + child: Text( + content.title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: fgColor, - )), - ), - IconButton( - icon: Icon(Icons.close, color: fgColor, size: 20), - onPressed: () => Navigator.of(ctx).pop(), - ), - ], - ), - Divider(color: dividerColor), - // Scrollable content - Flexible( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: widgets, + ), + ), + ), + IconButton( + icon: Icon(Icons.close, color: fgColor, size: 20), + onPressed: () => Navigator.of(ctx).pop(), + ), + ], + ), + Divider(color: dividerColor), + // Scrollable content + Flexible( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widgets, + ), ), ), - ), - ], + ], + ), ), ), ), @@ -367,7 +411,7 @@ class _HelpContent { final title = _extractTitle(raw) ?? 'Help'; // Extract tooltip text. - String tooltip = ''; + var tooltip = ''; if (tooltipIdx >= 0 && detailsIdx > tooltipIdx) { tooltip = raw.substring(tooltipIdx + tooltipMarker.length, detailsIdx).trim(); @@ -392,7 +436,7 @@ class _HelpContent { final blocks = <_DetailBlock>[]; final lines = raw.split('\n'); - for (int i = 0; i < lines.length; i++) { + for (var i = 0; i < lines.length; i++) { final line = lines[i]; final trimmed = line.trim(); @@ -428,10 +472,9 @@ class _HelpContent { .map((c) => c.trim()) .toList(); if (cells.length >= 2) { - blocks.add(_EntryBlock( - key: _stripInlineCode(cells[0]), - description: cells[1], - )); + blocks.add( + _EntryBlock(key: _stripInlineCode(cells[0]), description: cells[1]), + ); continue; } } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_client.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_client.dart index 6c1c59d69..155197562 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_client.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_client.dart @@ -13,8 +13,9 @@ // Author: Desmond Kirkpatrick import 'package:flutter/foundation.dart'; +import 'package:rohd_devtools_widgets/src/rohd_extension_status.dart'; + export 'rohd_extension_status.dart'; -import 'rohd_extension_status.dart'; /// Abstract client for the ROHD VS Code extension handshake. /// diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_status.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_status.dart index f87428254..551994fca 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_status.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_status.dart @@ -39,6 +39,7 @@ class RohdFormatInfo { /// Resolved absolute path to the file, if known. final String? path; + /// Creates availability information for a source format. const RohdFormatInfo({ required this.available, this.fileFound = false, @@ -92,6 +93,7 @@ class RohdModuleInfo { /// `true` while the extension is still loading an FST file asynchronously. final bool fstLoading; + /// Creates information returned by the ROHD extension for a module. const RohdModuleInfo({ required this.extensionAvailable, this.module, @@ -165,7 +167,9 @@ class RohdModuleInfo { final formats = {}; for (final entry in rawFormats.entries) { final fmt = _parseFormat(entry.key); - if (fmt == null) continue; + if (fmt == null) { + continue; + } final fmtMap = entry.value as Map? ?? const {}; formats[fmt] = RohdFormatInfo( available: fmtMap['available'] as bool? ?? false, diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/save_png_web.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/save_png_web.dart index 5b0b9221e..555d3cc0e 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/save_png_web.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/save_png_web.dart @@ -25,8 +25,9 @@ Future savePngBytes(Uint8List pngBytes, String fileName) async { ..download = fileName ..style.display = 'none'; web.document.body!.append(anchor); - anchor.click(); - anchor.remove(); + anchor + ..click() + ..remove(); web.URL.revokeObjectURL(url); return null; } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml index 3f24d6f92..4eae0d324 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml @@ -3,10 +3,11 @@ description: "Shared widgets for ROHD DevTools sub-packages: help button, PNG ex version: 0.1.0 publish_to: none environment: - sdk: '>=3.4.0 <4.0.0' - flutter: '>=3.0.0' + sdk: '>=3.6.0 <4.0.0' + flutter: '>=3.44.0' dependencies: flutter: {sdk: flutter} + material_ui: ^1.1.0 rohd: ^0.6.9 rohd_hierarchy: path: ../../../packages/rohd_hierarchy @@ -18,3 +19,7 @@ dev_dependencies: dependency_overrides: rohd: path: ../../.. + +flutter: + assets: + - assets/extension/ diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/app_bar_overlay_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/app_bar_overlay_test.dart index 080f41c11..d67fd81c9 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/app_bar_overlay_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/app_bar_overlay_test.dart @@ -19,14 +19,12 @@ void main() { child: Material(child: Text('Toolbar')), ); - testWidgets('lays out app bar above body when auto-hide is disabled', - (tester) async { + testWidgets('lays out app bar above body when auto-hide is disabled', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( - home: AppBarOverlay( - appBar: testAppBar(), - body: const Text('Body'), - ), + home: AppBarOverlay(appBar: testAppBar(), body: const Text('Body')), ), ); @@ -36,8 +34,9 @@ void main() { expect(find.byType(Stack), findsNothing); }); - testWidgets('slides overlay app bar in when pointer enters trigger zone', - (tester) async { + testWidgets('slides overlay app bar in when pointer enters trigger zone', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( home: AppBarOverlay( diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/bit_expansion_menu_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/bit_expansion_menu_test.dart index 921ebe684..5c21d5bf2 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/bit_expansion_menu_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/bit_expansion_menu_test.dart @@ -7,13 +7,14 @@ // 2026 July // Author: Desmond Kirkpatrick -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; void main() { - testWidgets('buildBitExpansionMenuItems creates divider and actions', - (tester) async { + testWidgets('buildBitExpansionMenuItems creates divider and actions', ( + tester, + ) async { final items = buildBitExpansionMenuItems( width: 16, includeDivider: true, @@ -43,8 +44,9 @@ void main() { expect(find.text('Define Bit Fields [16]...'), findsOneWidget); }); - testWidgets('resolveBitExpansionMenuValue expands small widths immediately', - (tester) async { + testWidgets('resolveBitExpansionMenuValue expands small widths immediately', ( + tester, + ) async { late BuildContext context; await tester.pumpWidget( MaterialApp( @@ -83,8 +85,9 @@ void main() { ); }); - testWidgets('resolveBitExpansionMenuValue uses dialogs for large ranges', - (tester) async { + testWidgets('resolveBitExpansionMenuValue uses dialogs for large ranges', ( + tester, + ) async { late BuildContext context; await tester.pumpWidget( MaterialApp( @@ -112,7 +115,7 @@ void main() { await tester.tap(find.text('OK')); await tester.pumpAndSettle(); - final range = await rangeFuture as BitExpandRangeAction; + final range = (await rangeFuture)! as BitExpandRangeAction; expect(range.bitStart, 3); expect(range.bitEnd, 6); @@ -127,7 +130,7 @@ void main() { await tester.tap(find.text('OK')); await tester.pumpAndSettle(); - final fields = await fieldsFuture as BitDefineFieldsAction; + final fields = (await fieldsFuture)! as BitDefineFieldsAction; expect(fields.fields.map((field) => field.name), ['upper', 'lower']); }); } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/bit_field_utils_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/bit_field_utils_test.dart index 3999dfe58..a72f62197 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/bit_field_utils_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/bit_field_utils_test.dart @@ -7,8 +7,8 @@ // 2026 July // Author: Desmond Kirkpatrick -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; void main() { @@ -37,17 +37,14 @@ void main() { group('parseBitFieldDefs', () { test('parses named and unnamed bit fields', () { - final fields = BitFieldUtils.parseBitFieldDefs( - ''' + final fields = BitFieldUtils.parseBitFieldDefs(''' exponent 31:21 mantissa 20:0 sign 31 7:4 0 ignored-name 3 -''', - 31, - ); +''', 31); expect(fields, hasLength(5)); expect(fields[0].name, 'exponent'); @@ -68,13 +65,10 @@ ignored-name 3 }); test('normalizes reversed and out-of-range indexes', () { - final fields = BitFieldUtils.parseBitFieldDefs( - ''' + final fields = BitFieldUtils.parseBitFieldDefs(''' low_high 1:8 too_high 40:32 -''', - 31, - ); +''', 31); expect(fields, hasLength(2)); expect(fields[0].high, 8); @@ -84,50 +78,53 @@ too_high 40:32 }); }); - testWidgets('showBitRangeDialog returns parsed input and cancel returns null', - (tester) async { - late BuildContext dialogContext; - await tester.pumpWidget( - MaterialApp( - theme: ThemeData( - useMaterial3: false, - splashFactory: NoSplash.splashFactory, + testWidgets( + 'showBitRangeDialog returns parsed input and cancel returns null', + (tester) async { + late BuildContext dialogContext; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: false, + splashFactory: NoSplash.splashFactory, + ), + home: Builder( + builder: (context) { + dialogContext = context; + return const SizedBox.shrink(); + }, + ), ), - home: Builder( - builder: (context) { - dialogContext = context; - return const SizedBox.shrink(); - }, - ), - ), - ); + ); - final future = showBitRangeDialog( - dialogContext, - signalName: 'data', - width: 16, - ); - await tester.pumpAndSettle(); - await tester.enterText(find.byType(TextField), '2:5'); - await tester.tap(find.text('OK')); - await tester.pumpAndSettle(); + final future = showBitRangeDialog( + dialogContext, + signalName: 'data', + width: 16, + ); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), '2:5'); + await tester.tap(find.text('OK')); + await tester.pumpAndSettle(); - expect(await future, (5, 2)); + expect(await future, (5, 2)); - final cancelled = showBitRangeDialog( - dialogContext, - signalName: 'data', - width: 16, - ); - await tester.pumpAndSettle(); - await tester.tap(find.text('Cancel')); - await tester.pumpAndSettle(); + final cancelled = showBitRangeDialog( + dialogContext, + signalName: 'data', + width: 16, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); - expect(await cancelled, isNull); - }); + expect(await cancelled, isNull); + }, + ); - testWidgets('showDefineBitFieldsDialog parses edited definitions', - (tester) async { + testWidgets('showDefineBitFieldsDialog parses edited definitions', ( + tester, + ) async { late BuildContext dialogContext; await tester.pumpWidget( MaterialApp( @@ -148,9 +145,7 @@ too_high 40:32 dialogContext, signalName: 'floatBits', width: 32, - existingDefs: const [ - BitFieldDef(name: 'sign', high: 31, low: 31), - ], + existingDefs: const [BitFieldDef(name: 'sign', high: 31, low: 31)], ); await tester.pumpAndSettle(); diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/capture_boundary_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/capture_boundary_test.dart index d1574c192..5a1d170e0 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/capture_boundary_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/capture_boundary_test.dart @@ -15,8 +15,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; void main() { - testWidgets('returns false when no repaint boundary is found', - (tester) async { + testWidgets('returns false when no repaint boundary is found', ( + tester, + ) async { final key = GlobalKey(); late BuildContext captureContext; @@ -37,8 +38,9 @@ void main() { ); }); - testWidgets('returns false when the key has no mounted context', - (tester) async { + testWidgets('returns false when the key has no mounted context', ( + tester, + ) async { final key = GlobalKey(); late BuildContext captureContext; @@ -59,8 +61,9 @@ void main() { ); }); - testWidgets('saves injected PNG bytes and shows saved path toast', - (tester) async { + testWidgets('saves injected PNG bytes and shows saved path toast', ( + tester, + ) async { final key = GlobalKey(); late BuildContext captureContext; Uint8List? savedBytes; @@ -100,15 +103,16 @@ void main() { find.byWidgetPredicate( (widget) => widget is Text && - widget.data?.startsWith('Saved: /tmp/wave_') == true, + (widget.data?.startsWith('Saved: /tmp/wave_') ?? false), ), findsOneWidget, ); await _letExportToastExpire(tester); }); - testWidgets('shows downloaded toast when save function returns no path', - (tester) async { + testWidgets('shows downloaded toast when save function returns no path', ( + tester, + ) async { final key = GlobalKey(); late BuildContext captureContext; @@ -132,15 +136,16 @@ void main() { find.byWidgetPredicate( (widget) => widget is Text && - widget.data?.startsWith('Downloaded capture_') == true, + (widget.data?.startsWith('Downloaded capture_') ?? false), ), findsOneWidget, ); await _letExportToastExpire(tester); }); - testWidgets('skips saved toast when context unmounts during save', - (tester) async { + testWidgets('skips saved toast when context unmounts during save', ( + tester, + ) async { final key = GlobalKey(); late BuildContext captureContext; @@ -166,8 +171,9 @@ void main() { expect(find.textContaining('Saved:'), findsNothing); }); - testWidgets('returns false and shows failure toast when save throws', - (tester) async { + testWidgets('returns false and shows failure toast when save throws', ( + tester, + ) async { final key = GlobalKey(); late BuildContext captureContext; @@ -186,13 +192,16 @@ void main() { await tester.pump(); expect(succeeded, isFalse); - expect(find.textContaining('Export failed: Bad state: disk full'), - findsOneWidget); + expect( + find.textContaining('Export failed: Bad state: disk full'), + findsOneWidget, + ); await _letExportToastExpire(tester); }); - testWidgets('returns false when injected encoder returns null', - (tester) async { + testWidgets('returns false when injected encoder returns null', ( + tester, + ) async { final key = GlobalKey(); late BuildContext captureContext; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_button_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_button_test.dart index 22df3efc5..4bb7060bb 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_button_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_button_test.dart @@ -21,10 +21,10 @@ void main() { await tester.pumpWidget( MaterialApp( theme: ThemeData( - useMaterial3: false, splashFactory: NoSplash.splashFactory), - home: Scaffold( - body: CrossProbeButton(service: service), + useMaterial3: false, + splashFactory: NoSplash.splashFactory, ), + home: Scaffold(body: CrossProbeButton(service: service)), ), ); diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_menu_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_menu_test.dart index 4da66a7af..2182cf360 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_menu_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_menu_test.dart @@ -7,17 +7,14 @@ // 2026 July // Author: Desmond Kirkpatrick -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; void main() { test('encodes, decodes, and labels source menu values', () { expect(gotoSourceMenuValue(RohdSourceFormat.rohd), 'goto_source:rohd'); - expect( - gotoSourceFormatFromValue('goto_source:sv'), - RohdSourceFormat.sv, - ); + expect(gotoSourceFormatFromValue('goto_source:sv'), RohdSourceFormat.sv); expect(gotoSourceFormatFromValue(null), isNull); expect(gotoSourceFormatFromValue('other'), isNull); expect(gotoSourceFormatFromValue('goto_source:missing'), isNull); @@ -26,10 +23,7 @@ void main() { expect(gotoSourceShortName(RohdSourceFormat.sv), 'SV'); expect(gotoSourceShortName(RohdSourceFormat.sc), 'SystemC'); expect(gotoSourceShortName(RohdSourceFormat.fst), 'Waveform'); - expect( - gotoSourceMenuLabel(RohdSourceFormat.rohd), - 'Go to ROHD Source', - ); + expect(gotoSourceMenuLabel(RohdSourceFormat.rohd), 'Go to ROHD Source'); expect( gotoSourceMenuLabel(RohdSourceFormat.sv, count: 3), 'Go to SV Source (3)', @@ -39,7 +33,7 @@ void main() { test('resolves default and exact navigable formats', () { expect(resolveNavigableFormats(null), kDefaultNavigableFormats); expect( - resolveNavigableFormats(const RohdModuleInfo(extensionAvailable: false)), + resolveNavigableFormats(RohdModuleInfo.unavailable), kDefaultNavigableFormats, ); expect( @@ -53,10 +47,7 @@ void main() { const RohdModuleInfo( extensionAvailable: true, formats: { - RohdSourceFormat.rohd: RohdFormatInfo( - available: true, - fileFound: false, - ), + RohdSourceFormat.rohd: RohdFormatInfo(available: true), RohdSourceFormat.sv: RohdFormatInfo( available: false, fileFound: true, @@ -67,53 +58,40 @@ void main() { isEmpty, ); - final info = RohdModuleInfo( + const info = RohdModuleInfo( extensionAvailable: true, formats: { - RohdSourceFormat.rohd: const RohdFormatInfo( - available: true, - fileFound: true, - ), - RohdSourceFormat.sc: const RohdFormatInfo( - available: true, - fileFound: true, - ), - RohdSourceFormat.fst: const RohdFormatInfo( - available: true, - fileFound: true, - ), + RohdSourceFormat.rohd: RohdFormatInfo(available: true, fileFound: true), + RohdSourceFormat.sc: RohdFormatInfo(available: true, fileFound: true), + RohdSourceFormat.fst: RohdFormatInfo(available: true, fileFound: true), }, ); - expect( - resolveNavigableFormats(info), - [RohdSourceFormat.rohd, RohdSourceFormat.sc], - ); + expect(resolveNavigableFormats(info), [ + RohdSourceFormat.rohd, + RohdSourceFormat.sc, + ]); }); - testWidgets('builds popup menu items with encoded values and custom icons', - (tester) async { + testWidgets('builds popup menu items with encoded values and custom icons', ( + tester, + ) async { final items = buildGotoSourceMenuItems( formats: [RohdSourceFormat.rohd, RohdSourceFormat.sv], count: 2, - showIcons: true, - iconBuilder: (format, {double size = 18}) => Icon( - Icons.code, - key: ValueKey(format), - size: size, - ), + iconBuilder: (format, {size = 18}) => + Icon(Icons.code, key: ValueKey(format), size: size), ); expect(items, hasLength(2)); + expect(items, everyElement(isA>())); expect(items[0].value, 'goto_source:rohd'); expect(items[1].value, 'goto_source:sv'); await tester.pumpWidget( MaterialApp( home: Material( - child: Column( - children: [for (final item in items) item.child!], - ), + child: Column(children: [for (final item in items) item.child!]), ), ), ); @@ -124,8 +102,9 @@ void main() { expect(find.byKey(const ValueKey(RohdSourceFormat.sv)), findsOneWidget); }); - testWidgets('builds rows, disabled menu items, and no-icon source items', - (tester) async { + testWidgets('builds rows, disabled menu items, and no-icon source items', ( + tester, + ) async { final disabled = buildRohdPopupMenuItem( value: 'disabled', icon: const Icon(Icons.block), @@ -168,8 +147,9 @@ void main() { expect(find.byIcon(Icons.timeline), findsOneWidget); }); - testWidgets('source format icons cover strips, aliases, and waveform icon', - (tester) async { + testWidgets('source format icons cover strips, aliases, and waveform icon', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( home: Material( @@ -185,7 +165,7 @@ void main() { ], size: 13, gap: 5, - iconBuilder: (format, {double size = 16}) => Icon( + iconBuilder: (format, {size = 16}) => Icon( Icons.code, key: ValueKey('strip-${format.name}'), size: size, diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_service_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_service_test.dart index 3f91273e8..262e273de 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_service_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/cross_probe_service_test.dart @@ -27,10 +27,7 @@ void main() { expect(schematic.incomingSignals.value, ['top.clk', 'top.reset']); expect(channel.lastSource, 'waveform'); expect(channel.lastPaths, ['top.clk', 'top.reset']); - expect( - () => channel.lastPaths!.add('top.extra'), - throwsUnsupportedError, - ); + expect(() => channel.lastPaths!.add('top.extra'), throwsUnsupportedError); }); test('does not broadcast while inactive or for empty selections', () { diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/export_button_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/export_button_test.dart index bdabebc52..4d330db6e 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/export_button_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/export_button_test.dart @@ -12,15 +12,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; void main() { - testWidgets('renders camera icon, tooltip, and invokes callback', - (tester) async { + testWidgets('renders camera icon, tooltip, and invokes callback', ( + tester, + ) async { var taps = 0; await tester.pumpWidget( MaterialApp( - theme: ThemeData( - splashFactory: NoSplash.splashFactory, - ), + theme: ThemeData(splashFactory: NoSplash.splashFactory), home: Scaffold( body: Center( child: ExportPngButton( @@ -33,8 +32,10 @@ void main() { ); expect(find.byIcon(Icons.camera_alt_outlined), findsOneWidget); - expect(tester.widget(find.byType(Tooltip)).message, - 'Save waveform PNG'); + expect( + tester.widget(find.byType(Tooltip)).message, + 'Save waveform PNG', + ); await tester.tap(find.byType(ExportPngButton)); await tester.pump(); diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/export_toast_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/export_toast_test.dart index b2fa49cf9..4a6592ed6 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/export_toast_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/export_toast_test.dart @@ -12,8 +12,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; void main() { - testWidgets('showExportToast inserts and removes an overlay entry', - (tester) async { + testWidgets('showExportToast inserts and removes an overlay entry', ( + tester, + ) async { late BuildContext toastContext; await tester.pumpWidget( MaterialApp( diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/logic_type_utils_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/logic_type_utils_test.dart index 86c9b6e68..bae71c8c2 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/logic_type_utils_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/logic_type_utils_test.dart @@ -36,13 +36,10 @@ void main() { }); test('expandLogicType slices contiguous array elements with LogicValue', () { - final nodes = expandLogicType( - { - 'arrayDims': [2], - 'elementWidth': 4, - }, - parentBinaryValue: '10101100', - ); + final nodes = expandLogicType({ + 'arrayDims': [2], + 'elementWidth': 4, + }, parentBinaryValue: '10101100'); expect(nodes, hasLength(2)); expect(nodes[0].value, '1100'); @@ -50,38 +47,35 @@ void main() { }); test('expandLogicType normalizes absolute struct bits and nested fields', () { - final nodes = expandLogicType( - { - 'typeName': 'Packet', - 'fields': [ - { - 'name': 'payload', - 'width': 4, - 'bits': [100, 101, 102, 103], - 'type': { - 'fields': [ - { - 'name': 'low', - 'width': 2, - 'bits': [0, 1], - }, - { - 'name': 'high', - 'width': 2, - 'bits': [2, 3], - }, - ], - }, - }, - { - 'name': 'valid', - 'width': 1, - 'bits': [104], + final nodes = expandLogicType({ + 'typeName': 'Packet', + 'fields': [ + { + 'name': 'payload', + 'width': 4, + 'bits': [100, 101, 102, 103], + 'type': { + 'fields': [ + { + 'name': 'low', + 'width': 2, + 'bits': [0, 1], + }, + { + 'name': 'high', + 'width': 2, + 'bits': [2, 3], + }, + ], }, - ], - }, - parentBinaryValue: '11010', - ); + }, + { + 'name': 'valid', + 'width': 1, + 'bits': [104], + }, + ], + }, parentBinaryValue: '11010'); expect(nodes, hasLength(2)); expect(nodes[0].name, 'payload'); @@ -96,13 +90,10 @@ void main() { }); test('expandLogicType expands multidimensional arrays', () { - final nodes = expandLogicType( - { - 'arrayDims': [2, 2], - 'elementWidth': 2, - }, - parentBinaryValue: '11100100', - ); + final nodes = expandLogicType({ + 'arrayDims': [2, 2], + 'elementWidth': 2, + }, parentBinaryValue: '11100100'); expect(nodes, hasLength(2)); expect(nodes[0].name, '[0]'); @@ -113,48 +104,48 @@ void main() { expect(nodes[1].children.map((child) => child.value), ['10', '11']); }); - test('formatTypeTooltip includes signal name, type, values, and depth limit', - () { - final tooltip = formatTypeTooltip( - { - 'typeName': 'Packet', - 'fields': [ - { - 'name': 'payload', - 'width': 4, - 'bits': [0, 1, 2, 3], - 'type': { - 'fields': [ - { - 'name': 'nibble', - 'width': 4, - 'bits': [0, 1, 2, 3], - 'type': { - 'fields': [ - { - 'name': 'bit0', - 'width': 1, - 'bits': [0], - }, - ], + test( + 'formatTypeTooltip includes signal name, type, values, and depth limit', + () { + final tooltip = formatTypeTooltip( + { + 'typeName': 'Packet', + 'fields': [ + { + 'name': 'payload', + 'width': 4, + 'bits': [0, 1, 2, 3], + 'type': { + 'fields': [ + { + 'name': 'nibble', + 'width': 4, + 'bits': [0, 1, 2, 3], + 'type': { + 'fields': [ + { + 'name': 'bit0', + 'width': 1, + 'bits': [0], + }, + ], + }, }, - }, - ], + ], + }, }, - }, - ], - }, - parentBinaryValue: '1010', - signalName: 'packet', - maxDepth: 2, - ); + ], + }, + parentBinaryValue: '1010', + signalName: 'packet', + maxDepth: 2, + ); - expect( - tooltip, - '''packet (Packet) + expect(tooltip, ''' +packet (Packet) payload: 4'b1010 nibble: 4'b1010 - ...''', - ); - }); + ...'''); + }, + ); } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/markdown_help_button_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/markdown_help_button_test.dart index c1731db48..8f3e8b8b8 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/markdown_help_button_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/markdown_help_button_test.dart @@ -50,8 +50,9 @@ second line expect(tester.widget(find.byType(Tooltip)).message, expected); } - testWidgets('loads markdown, applies substitutions, and renders dialog', - (tester) async { + testWidgets('loads markdown, applies substitutions, and renders dialog', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( home: DefaultAssetBundle( @@ -83,30 +84,32 @@ second line expect(find.text('First line second line'), findsOneWidget); }); - testWidgets('falls back to bare asset path when package asset is unavailable', - (tester) async { - await tester.pumpWidget( - MaterialApp( - home: DefaultAssetBundle( - bundle: _MapAssetBundle({assetPath: markdown}), - child: const MarkdownHelpButton( - assetPath: assetPath, - package: 'rohd_devtools_widgets', - isDark: true, - labelIcon: Icon(Icons.help_outline), - substitutions: {'VERSION': '2.0.0', 'THING': 'signals'}, + testWidgets( + 'falls back to bare asset path when package asset is unavailable', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: DefaultAssetBundle( + bundle: _MapAssetBundle({assetPath: markdown}), + child: const MarkdownHelpButton( + assetPath: assetPath, + package: 'rohd_devtools_widgets', + isDark: true, + labelIcon: Icon(Icons.help_outline), + substitutions: {'VERSION': '2.0.0', 'THING': 'signals'}, + ), ), ), - ), - ); - await pumpUntilTooltipMessage(tester, 'Quick help for signals'); - - expect(find.byIcon(Icons.help_outline), findsOneWidget); - expect( - tester.widget(find.byType(Tooltip)).message, - 'Quick help for signals', - ); - }); + ); + await pumpUntilTooltipMessage(tester, 'Quick help for signals'); + + expect(find.byIcon(Icons.help_outline), findsOneWidget); + expect( + tester.widget(find.byType(Tooltip)).message, + 'Quick help for signals', + ); + }, + ); } class _MapAssetBundle extends CachingAssetBundle { diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/rohd_extension_status_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/rohd_extension_status_test.dart index d340c0c45..ada6db403 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/rohd_extension_status_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/rohd_extension_status_test.dart @@ -86,7 +86,9 @@ void main() { expect(RohdModuleInfo.formatLabel(RohdSourceFormat.sv), 'SystemVerilog'); expect(RohdModuleInfo.formatLabel(RohdSourceFormat.sc), 'SystemC'); expect( - RohdModuleInfo.formatLabel(RohdSourceFormat.fst), 'Waveform (FST)'); + RohdModuleInfo.formatLabel(RohdSourceFormat.fst), + 'Waveform (FST)', + ); const unavailable = RohdModuleInfo.unavailable; expect(unavailable.extensionAvailable, isFalse); diff --git a/rohd_devtools_extension/tool/gh_actions/build_app.sh b/rohd_devtools_extension/tool/gh_actions/build_app.sh new file mode 100755 index 000000000..072b23e2b --- /dev/null +++ b/rohd_devtools_extension/tool/gh_actions/build_app.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# Builds the standalone ROHD DevTools application for static hosting. +# +# Usage: +# tool/gh_actions/build_app.sh [base-href] + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +base_href="${1:-}" + +if [[ -z "$base_href" ]]; then + if [[ -n "${GITHUB_REPOSITORY:-}" ]]; then + owner="${GITHUB_REPOSITORY%%/*}" + repository="${GITHUB_REPOSITORY#*/}" + if [[ "$repository" == "$owner.github.io" ]]; then + base_href="/" + else + base_href="/$repository/" + fi + else + base_href="/rohd_devtools_extension/" + fi +fi + +if [[ ! "$base_href" =~ ^/[A-Za-z0-9._/-]*/$ ]]; then + echo "error: base href must start and end with '/': $base_href" >&2 + exit 64 +fi + +cd "$repo_root" + +make web-release \ + FLUTTER_WEB_BUILD_ARGS="--pwa-strategy=none --base-href=$base_href" + +rm -f build/web/flutter_service_worker.js + +required_files=( + build/web/index.html + build/web/flutter_bootstrap.js + build/web/main.dart.js + build/web/assets/AssetManifest.bin +) + +for required_file in "${required_files[@]}"; do + if [[ ! -f "$required_file" ]]; then + echo "error: expected static app artifact not found: $required_file" >&2 + exit 1 + fi +done + +if grep -Fq 'serviceWorkerSettings: {' build/web/flutter_bootstrap.js; then + echo "error: Flutter bootstrap unexpectedly registers a service worker." >&2 + exit 1 +fi + +if ! grep -Fq "" build/web/index.html; then + echo "error: generated index.html does not use base href $base_href" >&2 + exit 1 +fi + +touch build/web/.nojekyll + +echo "Standalone app ready in build/web/ (base href: $base_href)." diff --git a/rohd_devtools_extension/tool/test_devtools_install.dart b/rohd_devtools_extension/tool/test_devtools_install.dart index 01e6a1c04..f04097f14 100644 --- a/rohd_devtools_extension/tool/test_devtools_install.dart +++ b/rohd_devtools_extension/tool/test_devtools_install.dart @@ -61,7 +61,7 @@ Future main(List args) async { _requireFile(extensionAssetsPath, 'flutter.js'); _requireFile(extensionAssetsPath, 'main.dart.js'); _requireFile(extensionAssetsPath, 'version.json'); - _requireFile(extensionAssetsPath, p.join('assets', 'AssetManifest.json')); + _requireFile(extensionAssetsPath, p.join('assets', 'AssetManifest.bin')); _requireFile(extensionAssetsPath, p.join('assets', 'FontManifest.json')); _requireFile(extensionAssetsPath, p.join('canvaskit', 'canvaskit.js')); _requireFile(extensionAssetsPath, p.join('canvaskit', 'canvaskit.wasm'));