feat(studio): HubService.invokePluginTheme + integration test
Wraps the new InvokePluginTheme RPC and ships an end-to-end test that loads studio-theme-solarized into a fresh HubFixture and asserts the light/dark schemes round-trip. Signed-off-by: flemming-it <sf@flemming.it> Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
efa9871a75
commit
a5bcde6446
2 changed files with 112 additions and 0 deletions
|
|
@ -396,6 +396,30 @@ class HubService {
|
||||||
return (state: state, text: r.text, sourcePath: r.sourcePath);
|
return (state: state, text: r.text, sourcePath: r.sourcePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Invoke a Studio theme plugin's `theme` hook and unwrap
|
||||||
|
/// the result into Flutter-friendly `ColorScheme` slots.
|
||||||
|
///
|
||||||
|
/// Returns a record:
|
||||||
|
/// - `brightness`: echo of the input.
|
||||||
|
/// - `tokens`: 14 32-bit ARGB values in Material 3
|
||||||
|
/// order. Caller passes them to
|
||||||
|
/// `ColorScheme(...)` after deciding the
|
||||||
|
/// `brightness` enum.
|
||||||
|
///
|
||||||
|
/// Throws on hub-side errors (plugin not installed, plugin
|
||||||
|
/// declined the brightness, etc.) — caller routes the
|
||||||
|
/// exception through `friendlyError` like every other RPC.
|
||||||
|
Future<({String brightness, List<int> tokens})> invokePluginTheme({
|
||||||
|
required String capability,
|
||||||
|
required String brightness,
|
||||||
|
}) async {
|
||||||
|
final r = await _client.invokePluginTheme(
|
||||||
|
capability: capability,
|
||||||
|
brightness: brightness,
|
||||||
|
);
|
||||||
|
return (brightness: r.brightness, tokens: r.tokens.toList());
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapshot of every configured MCP server.
|
/// Snapshot of every configured MCP server.
|
||||||
Future<List<McpClientInfo>> listMcpClients() async {
|
Future<List<McpClientInfo>> listMcpClients() async {
|
||||||
final r = await _client.listMcpClients();
|
final r = await _client.listMcpClients();
|
||||||
|
|
|
||||||
88
test/integration/plugin_theme_test.dart
Normal file
88
test/integration/plugin_theme_test.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// End-to-end integration test for the Studio-plugin theme
|
||||||
|
// hook: spins a real `fai serve`, installs the in-tree
|
||||||
|
// `studio-theme-solarized` plugin via direct module-dir
|
||||||
|
// staging (mirror-install would also work but takes longer
|
||||||
|
// and pulls a network egress), then calls `invokePluginTheme`
|
||||||
|
// and asserts the round-trip.
|
||||||
|
//
|
||||||
|
// Run manually with:
|
||||||
|
//
|
||||||
|
// flutter test test/integration/plugin_theme_test.dart
|
||||||
|
//
|
||||||
|
// Pre-reqs (handled by HubFixture's skip-when-missing path):
|
||||||
|
// - fai binary on PATH or at ../fai_platform/target/release/fai
|
||||||
|
// - studio-theme-solarized plugin built to module.wasm in its
|
||||||
|
// fai_modules dir.
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'hub_fixture.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test(
|
||||||
|
'invokePluginTheme returns Solarized Light + Dark schemes',
|
||||||
|
() async {
|
||||||
|
final fixture = await HubFixture.start();
|
||||||
|
if (fixture == null) return;
|
||||||
|
try {
|
||||||
|
// Stage the plugin into the fixture's modules dir
|
||||||
|
// ourselves. Going through `installModule` would
|
||||||
|
// require the plugin to be on the mirror, which would
|
||||||
|
// add network + bundle-fetch latency that doesn't
|
||||||
|
// belong in an in-process round-trip test.
|
||||||
|
final pluginSrc = Directory(
|
||||||
|
'../fai_modules/studio-theme-solarized',
|
||||||
|
);
|
||||||
|
if (!await pluginSrc.exists()) {
|
||||||
|
markTestSkipped('studio-theme-solarized source not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final wasm = File('${pluginSrc.path}/module.wasm');
|
||||||
|
if (!await wasm.exists()) {
|
||||||
|
markTestSkipped(
|
||||||
|
'studio-theme-solarized has no built module.wasm — '
|
||||||
|
'run `cargo build --release --target wasm32-wasip2` first',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final dest = Directory(
|
||||||
|
'${fixture.tempDir.path}/modules/studio-theme-solarized',
|
||||||
|
);
|
||||||
|
await dest.create(recursive: true);
|
||||||
|
await File('${pluginSrc.path}/module.yaml')
|
||||||
|
.copy('${dest.path}/module.yaml');
|
||||||
|
await wasm.copy('${dest.path}/module.wasm');
|
||||||
|
|
||||||
|
// Call the hook for both brightnesses.
|
||||||
|
final light = await fixture.client.invokePluginTheme(
|
||||||
|
capability: 'studio.theme.solarized',
|
||||||
|
brightness: 'light',
|
||||||
|
);
|
||||||
|
expect(light.brightness, 'light');
|
||||||
|
expect(light.tokens.length, 14);
|
||||||
|
// Solarized blue is the pinned primary in both schemes.
|
||||||
|
expect(light.tokens[0], 0xFF268BD2);
|
||||||
|
|
||||||
|
final dark = await fixture.client.invokePluginTheme(
|
||||||
|
capability: 'studio.theme.solarized',
|
||||||
|
brightness: 'dark',
|
||||||
|
);
|
||||||
|
expect(dark.brightness, 'dark');
|
||||||
|
expect(dark.tokens.length, 14);
|
||||||
|
expect(dark.tokens[0], 0xFF268BD2);
|
||||||
|
|
||||||
|
// Different surface tokens between Light and Dark —
|
||||||
|
// regression guard against a plugin that returns the
|
||||||
|
// same scheme for both.
|
||||||
|
expect(light.tokens[8], isNot(equals(dark.tokens[8])));
|
||||||
|
} finally {
|
||||||
|
await fixture.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Component-Model instantiation can take a few seconds on
|
||||||
|
// first run as wasmtime caches the compiled artefact.
|
||||||
|
timeout: const Timeout(Duration(minutes: 1)),
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue