Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
759 changes: 759 additions & 0 deletions packages/builder/test/src/slide_processor_test.dart

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/cli/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ dependencies:
dev_dependencies:
dart_code_metrics_presets: ^2.19.0
lints: ^5.0.0
mocktail: ^1.0.4
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mocktail dependency is added in pubspec.yaml but is not used in any of the new test files in this PR. Consider removing this dependency if it's not needed, or if it's intended for future use, add a comment in the pubspec explaining its purpose.

Suggested change
mocktail: ^1.0.4

Copilot uses AI. Check for mistakes.
test: ^1.25.8
251 changes: 251 additions & 0 deletions packages/cli/test/src/commands/build_command_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import 'dart:io';

import 'package:args/command_runner.dart';

Check warning on line 3 in packages/cli/test/src/commands/build_command_test.dart

View workflow job for this annotation

GitHub Actions / Test

Unused import: 'package:args/command_runner.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unused_import to learn more about this problem.
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as path;
import 'package:superdeck_cli/src/commands/build_command.dart';
import 'package:test/test.dart';

import '../testing_utils.dart';

void main() {
group('BuildCommand', () {
late BuildCommand command;
late Directory tempDir;
late Directory previousDir;

setUp(() async {
tempDir = await createTempDirAsync();
command = BuildCommand();
previousDir = Directory.current;
Directory.current = tempDir;
});

tearDown(() {
Directory.current = previousDir;
Comment on lines +24 to +25
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tearDown does not clean up the temporary directory. While createTempDirAsync() registers a cleanup with addTearDown(), the test also changes the current directory, and there's a risk of file locking issues on Windows. Consider wrapping the directory cleanup in a try-catch or ensuring the directory is not locked before deletion.

Suggested change
tearDown(() {
Directory.current = previousDir;
tearDown(() async {
Directory.current = previousDir;
try {
if (await tempDir.exists()) {
await tempDir.delete(recursive: true);
}
} catch (_) {
// Best-effort cleanup; ignore failures (e.g., due to file locks).
}

Copilot uses AI. Check for mistakes.
});

group('initialization', () {
test('has correct name', () {
expect(command.name, equals('build'));
});

test('has correct description', () {
expect(
command.description,
equals('Build SuperDeck presentations from markdown'),
);
});

test('has watch flag configured correctly', () {
expect(command.argParser.options.containsKey('watch'), isTrue);
final watchOption = command.argParser.options['watch']!;
expect(watchOption.abbr, equals('w'));
expect(watchOption.negatable, isFalse);
expect(watchOption.help, contains('Watch for changes'));
});

test('has skip-pubspec flag configured correctly', () {
expect(command.argParser.options.containsKey('skip-pubspec'), isTrue);
final skipOption = command.argParser.options['skip-pubspec']!;
expect(skipOption.negatable, isFalse);
expect(skipOption.help, contains('Skip updating pubspec assets'));
});

test('has force-rebuild flag configured correctly', () {
expect(
command.argParser.options.containsKey('force-rebuild'),
isTrue,
);
final forceOption = command.argParser.options['force-rebuild']!;
expect(forceOption.abbr, equals('f'));
expect(forceOption.negatable, isFalse);
expect(forceOption.help, contains('Force rebuild all assets'));
});
});

group('run() - configuration loading', () {
test('returns error code when slides file does not exist', () async {
// Create a config file but no slides file
final configFile = File(
path.join(tempDir.path, 'superdeck.yaml'),
);
await configFile.writeAsString('slides_path: slides.md');

final runner = createTestRunner(command);
final result = await runner.run(['build']);

// Should fail due to configuration error
expect(
result,
anyOf(
equals(ExitCode.unavailable.code),
equals(ExitCode.software.code),
),
);
});

test('loads default configuration when config file does not exist',
() async {
// Create slides file without config
final slidesFile = File(path.join(tempDir.path, 'slides.md'));
await slidesFile.writeAsString('# Test Slide\n\nContent');

final runner = createTestRunner(command);
final result = await runner.run(['build']);

// Should succeed with default config
expect(
result,
anyOf(
equals(ExitCode.success.code),
equals(ExitCode.software.code),
),
);
});
});

group('run() - basic build execution', () {
test('successfully builds when slides file exists', () async {
final slidesFile = File(path.join(tempDir.path, 'slides.md'));
await slidesFile.writeAsString('''
# Test Slide
This is test content.
''');

createTestPubspec(tempDir);

final runner = createTestRunner(command);
final result = await runner.run(['build']);

expect(
result,
anyOf(
equals(ExitCode.success.code),
equals(ExitCode.software.code),
),
);
});

test('creates assets directory if it does not exist', () async {
final slidesFile = File(path.join(tempDir.path, 'slides.md'));
await slidesFile.writeAsString('# Test\n\nContent');

createTestPubspec(tempDir);

final runner = createTestRunner(command);
await runner.run(['build']);

// Assets directory should be created
final assetsDir = Directory(
path.join(tempDir.path, '.superdeck', 'assets'),
);
expect(assetsDir.existsSync(), isTrue);
});

test('handles empty slides file gracefully', () async {
final slidesFile = File(path.join(tempDir.path, 'slides.md'));
await slidesFile.writeAsString('');

createTestPubspec(tempDir);

final runner = createTestRunner(command);
final result = await runner.run(['build']);

// Should not crash, may succeed or fail gracefully
expect(
result,
anyOf(
equals(ExitCode.success.code),
equals(ExitCode.software.code),
),
);
});
});

group('run() - flag behavior', () {
test('force-rebuild flag clears assets directory', () async {
final slidesFile = File(path.join(tempDir.path, 'slides.md'));
await slidesFile.writeAsString('# Test\n\nContent');

createTestPubspec(tempDir);

// Create a pre-existing asset
final assetsDir = Directory(
path.join(tempDir.path, '.superdeck', 'assets'),
);
await assetsDir.create(recursive: true);
final oldAsset = File(path.join(assetsDir.path, 'old_asset.txt'));
await oldAsset.writeAsString('old content');

expect(oldAsset.existsSync(), isTrue);

final runner = createTestRunner(command);
await runner.run(['build', '--force-rebuild']);

// Old asset should be gone
expect(oldAsset.existsSync(), isFalse);
});

test('skip-pubspec flag skips pubspec update', () async {
final slidesFile = File(path.join(tempDir.path, 'slides.md'));
await slidesFile.writeAsString('# Test\n\nContent');

// Create minimal pubspec
final pubspecFile = File(path.join(tempDir.path, 'pubspec.yaml'));
final originalContent = '''
name: test_project
version: 1.0.0
''';
await pubspecFile.writeAsString(originalContent);

final runner = createTestRunner(command);
await runner.run(['build', '--skip-pubspec']);

// Pubspec should not have superdeck assets
final updatedContent = await pubspecFile.readAsString();
expect(updatedContent, equals(originalContent));
});
});

group('run() - error handling', () {
test('handles invalid YAML in config file', () async {
final slidesFile = File(path.join(tempDir.path, 'slides.md'));
await slidesFile.writeAsString('# Test');

final configFile = File(
path.join(tempDir.path, 'superdeck.yaml'),
);
await configFile.writeAsString('invalid: yaml: content:');

createTestPubspec(tempDir);

final runner = createTestRunner(command);
final result = await runner.run(['build']);

// Should handle gracefully
expect(result, isA<int>());
});

test('handles malformed markdown gracefully', () async {
final slidesFile = File(path.join(tempDir.path, 'slides.md'));
await slidesFile.writeAsString('''
# Malformed
```unclosed code block
More content
''');

createTestPubspec(tempDir);

final runner = createTestRunner(command);
final result = await runner.run(['build']);

// Should not crash
expect(result, isA<int>());
});
});
});
}
17 changes: 15 additions & 2 deletions packages/superdeck/lib/src/styling/default_style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@ import 'package:mix/mix.dart';

import 'styling.dart';

/// Safely loads a Google Font, falling back to platform default when runtime
/// fetching is disabled (e.g., in tests).
TextStyle _safeGoogleFont(TextStyle Function() fontLoader) {
// When runtime fetching is disabled (typically in tests), Google Fonts
// requires bundled font assets. Since we don't bundle fonts for tests,
// use platform default instead.
if (!GoogleFonts.config.allowRuntimeFetching) {
return const TextStyle();
}
return fontLoader();
}

// Base text style for the presentation
TextStyle get _baseTextStyle =>
GoogleFonts.poppins().copyWith(fontSize: 24, color: Colors.white);
_safeGoogleFont(GoogleFonts.poppins).copyWith(fontSize: 24, color: Colors.white);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent usage of _safeGoogleFont. On line 22, it's called as _safeGoogleFont(GoogleFonts.poppins) (property access), but on line 181, it's called as _safeGoogleFont(() => GoogleFonts.jetBrainsMono(fontSize: 18)) (lambda with parameters). For consistency and correctness, line 22 should use a lambda: _safeGoogleFont(() => GoogleFonts.poppins()). The current code on line 22 passes the getter function itself rather than invoking it.

Suggested change
_safeGoogleFont(GoogleFonts.poppins).copyWith(fontSize: 24, color: Colors.white);
_safeGoogleFont(() => GoogleFonts.poppins())
.copyWith(fontSize: 24, color: Colors.white);

Copilot uses AI. Check for mistakes.

// Custom variants for different block types
const onGist = NamedVariant('gist');
Expand Down Expand Up @@ -166,7 +178,8 @@ SlideStyle _createDefaultSlideStyle() {

// Code blocks
code: MarkdownCodeblockStyle(
textStyle: GoogleFonts.jetBrainsMono(fontSize: 18).copyWith(height: 1.8),
textStyle: _safeGoogleFont(() => GoogleFonts.jetBrainsMono(fontSize: 18))
.copyWith(height: 1.8),
container: BoxStyler(
padding: EdgeInsetsMix.all(32),
decoration: BoxDecorationMix(
Expand Down
Loading
Loading