[vlc-commits] [Git][videolan/vlc][master] 13 commits: macosx: Add test for VLCHexNumberFormatterTest

Steve Lhomme (@robUx4) gitlab at videolan.org
Wed Aug 26 07:48:21 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
887ac91b by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add test for VLCHexNumberFormatterTest

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
5dc7d2b1 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Fix time formatter string object -> number calc

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
39b81c3a by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add VLCTimeFormatter test

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
cab7dd1b by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add NSString+Helpers tests

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
5b61e724 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Fix time parsing for exactly 3600 seconds

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
ce380c02 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add log message test

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
e7471e9b by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Check for info validity before access in log message

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
c30bda26 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add basic initial test for VLCInputItem

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
c6173ddc by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add input node test

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
7626a3ee by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Extract VLCInputNode to own header/implementation files

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
6ddac818 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add string equality check that also matches nullability

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
6ab28918 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Use nil-inclusive equality check for input item state differentiation check

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
44c4002e by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Expand input item testing (parsing, notifications, thumbnail states, differing states)

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -


25 changed files:

- modules/gui/macosx/Makefile.am
- modules/gui/macosx/extensions/NSString+Helpers.h
- modules/gui/macosx/extensions/NSString+Helpers.m
- modules/gui/macosx/library/VLCInputItem.h
- modules/gui/macosx/library/VLCInputItem.m
- + modules/gui/macosx/library/VLCInputNode.h
- + modules/gui/macosx/library/VLCInputNode.m
- modules/gui/macosx/library/VLCInputNodePathControlItem.m
- modules/gui/macosx/library/VLCLibrarySegment.m
- modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationStack.m
- modules/gui/macosx/library/media-source/VLCLocalMediaSource.m
- modules/gui/macosx/library/media-source/VLCMediaSource.m
- modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
- modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
- modules/gui/macosx/tests/Makefile.am
- + modules/gui/macosx/tests/NSStringHelpersTest.m
- + modules/gui/macosx/tests/VLCHexNumberFormatterTest.m
- + modules/gui/macosx/tests/VLCInputItemTest.m
- + modules/gui/macosx/tests/VLCInputItemTestSupport.h
- + modules/gui/macosx/tests/VLCInputItemTestSupport.m
- + modules/gui/macosx/tests/VLCInputNodeTest.m
- + modules/gui/macosx/tests/VLCLogMessageTest.m
- + modules/gui/macosx/tests/VLCTimeFormatterTest.m
- modules/gui/macosx/views/VLCTimeFormatter.m
- modules/gui/macosx/windows/logging/VLCLogMessage.m


Changes:

=====================================
modules/gui/macosx/Makefile.am
=====================================
@@ -147,6 +147,8 @@ libmacosx_plugin_la_SOURCES = \
 	gui/macosx/imported/SPMediaKeyTap/SPMediaKeyTap.m \
 	gui/macosx/library/VLCInputItem.h \
 	gui/macosx/library/VLCInputItem.m \
+	gui/macosx/library/VLCInputNode.h \
+	gui/macosx/library/VLCInputNode.m \
 	gui/macosx/library/VLCInputNodePathControl.h \
 	gui/macosx/library/VLCInputNodePathControl.m \
 	gui/macosx/library/VLCInputNodePathControlItem.h \


=====================================
modules/gui/macosx/extensions/NSString+Helpers.h
=====================================
@@ -74,6 +74,12 @@ bool fixIntfSettings(void);
 
 @interface NSString (Helpers)
 
+/**
+ Compares two strings by value, treating two nil strings as equal.
+ */
++ (BOOL)equalOrBothNil:(nullable NSString *)firstString
+                    to:(nullable NSString *)secondString;
+
 /**
  Creates an NSString with the current time of the \c input_item_t
 


=====================================
modules/gui/macosx/extensions/NSString+Helpers.m
=====================================
@@ -79,6 +79,16 @@ NSString * flagEmojiStringForCountryCode(NSString *countryCode)
 
 @implementation NSString (Helpers)
 
++ (BOOL)equalOrBothNil:(nullable NSString *)firstString
+                    to:(nullable NSString *)secondString
+{
+    if (firstString == nil || secondString == nil) {
+        return firstString == nil && secondString == nil;
+    }
+
+    return [firstString isEqualToString:secondString];
+}
+
 + (instancetype)stringWithDuration:(vlc_tick_t)duration
                        currentTime:(vlc_tick_t)time
                           negative:(BOOL)negative
@@ -115,7 +125,7 @@ NSString * flagEmojiStringForCountryCode(NSString *countryCode)
 + (instancetype)stringWithTime:(long long int)time
 {
     if (time > 0) {
-        if (time > 3600)
+        if (time >= 3600)
             return [NSString stringWithFormat:@"%s%01ld:%02ld:%02ld",
                     time < 0 ? "-" : "",
                     (long) (time / 3600),


=====================================
modules/gui/macosx/library/VLCInputItem.h
=====================================
@@ -95,17 +95,4 @@ extern NSString * const VLCInputItemCommonDataDifferingFlagString;
 
 NSDictionary<NSString *, id> *commonInputItemData(NSArray<VLCInputItem*> * const inputItems);
 
- at interface VLCInputNode : NSObject
-
-- (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode;
-
- at property (readonly) struct input_item_node_t *vlcInputItemNode;
- at property (readonly, nullable) VLCInputItem *inputItem;
- at property (readonly) int numberOfChildren;
- at property (readonly, nullable) NSArray <VLCInputNode *> *children;
-
-- (void)clearChildrenCache;
-
- at end
-
 NS_ASSUME_NONNULL_END


=====================================
modules/gui/macosx/library/VLCInputItem.m
=====================================
@@ -596,7 +596,8 @@ BOOL differing_##prop = NO;
 NSString * const firstItem_##prop = firstInputItem.prop;
 
 #define UPDATE_IF_DIFFERING_BOOL(prop)                                                          \
-differing_##prop = differing_##prop || ![inputItem.prop isEqualToString:firstItem_##prop];
+differing_##prop = differing_##prop || ![NSString equalOrBothNil:inputItem.prop                \
+                                                                   to:firstItem_##prop];
 
 #define ADD_PROP_TO_DICT(prop)                                                              \
 NSString * firstItemValue_##prop = firstItem_##prop == nil ? @"" : firstItem_##prop;        \
@@ -615,7 +616,8 @@ NSString * const value_##prop =
 
         PERFORM_ACTION_PER_INPUTITEM_NSSTRING_PROP(UPDATE_IF_DIFFERING_BOOL);
         NSString * const inputItem_artworkURL = inputItem.artworkURL.absoluteString;
-        differing_artworkURL |= ![inputItem_artworkURL isEqualToString:firstItem_artworkURL];
+        differing_artworkURL |= ![NSString equalOrBothNil:inputItem_artworkURL
+                                                          to:firstItem_artworkURL];
     }
 
     PERFORM_ACTION_PER_INPUTITEM_PROP(ADD_PROP_TO_DICT);
@@ -628,73 +630,3 @@ NSString * const value_##prop =
 
     return [commonData copy];
 }
-
-
- at interface VLCInputNode ()
-{
-    NSArray<VLCInputNode *> *_cachedChildren;
-}
- at end
-
- at implementation VLCInputNode
-
-- (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode
-{
-    self = [super init];
-    if (self && p_inputNode != NULL) {
-        _vlcInputItemNode = p_inputNode;
-
-        if (_vlcInputItemNode->p_item) {
-            _inputItem = [[VLCInputItem alloc] initWithInputItem:_vlcInputItemNode->p_item];
-        }
-    }
-    return self;
-}
-
-- (NSString *)description
-{
-    NSString *inputItemName;
-    if (_vlcInputItemNode && _vlcInputItemNode->p_item)
-        inputItemName = toNSStr(_vlcInputItemNode->p_item->psz_name);
-    else
-        inputItemName = @"p_item == nil";
-    return [NSString stringWithFormat:@"%@: node: %p input name: %@, number of children: %i", NSStringFromClass([self class]),_vlcInputItemNode, inputItemName, self.numberOfChildren];
-}
-
-- (void)clearChildrenCache
-{
-    _cachedChildren = nil;
-}
-
-- (int)numberOfChildren
-{
-    if (_cachedChildren) {
-        return (int)_cachedChildren.count;
-    }
-    return _vlcInputItemNode ? _vlcInputItemNode->i_children : 0;
-}
-
-- (nullable NSArray<VLCInputNode *> *)children
-{
-    if (_cachedChildren) {
-        return _cachedChildren;
-    }
-
-    if (_vlcInputItemNode == NULL) {
-        return nil;
-    }
-    NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:_vlcInputItemNode->i_children];
-    for (int i = 0; i < _vlcInputItemNode->i_children; i++) {
-        if (_vlcInputItemNode->pp_children == NULL) {
-            break;
-        }
-        VLCInputNode *inputNode = [[VLCInputNode alloc] initWithInputNode:_vlcInputItemNode->pp_children[i]];
-        if (inputNode) {
-            [mutableArray addObject:inputNode];
-        }
-    }
-    _cachedChildren = [mutableArray copy];
-    return _cachedChildren;
-}
-
- at end


=====================================
modules/gui/macosx/library/VLCInputNode.h
=====================================
@@ -0,0 +1,37 @@
+/****************************************************************************
+ * VLCInputNode.h: MacOS X interface module
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Felix Paul Kühne <fkuehne # videolan -dot- org>
+ *          Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version.
+ *****************************************************************************/
+
+#import <Foundation/Foundation.h>
+
+#import <vlc_common.h>
+#import <vlc_input_item.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+ at class VLCInputItem;
+
+ at interface VLCInputNode : NSObject
+
+- (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode;
+
+ at property (readonly) struct input_item_node_t *vlcInputItemNode;
+ at property (readonly, nullable) VLCInputItem *inputItem;
+ at property (readonly) int numberOfChildren;
+ at property (readonly, nullable) NSArray <VLCInputNode *> *children;
+
+- (void)clearChildrenCache;
+
+ at end
+
+NS_ASSUME_NONNULL_END


=====================================
modules/gui/macosx/library/VLCInputNode.m
=====================================
@@ -0,0 +1,88 @@
+/****************************************************************************
+ * VLCInputNode.m: MacOS X interface module
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Felix Paul Kühne <fkuehne # videolan -dot- org>
+ *          Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version.
+ *****************************************************************************/
+
+#import "VLCInputNode.h"
+
+#import "VLCInputItem.h"
+
+#import "extensions/NSString+Helpers.h"
+
+ at interface VLCInputNode ()
+{
+    NSArray<VLCInputNode *> *_cachedChildren;
+}
+ at end
+
+ at implementation VLCInputNode
+
+- (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode
+{
+    self = [super init];
+    if (self && p_inputNode != NULL) {
+        _vlcInputItemNode = p_inputNode;
+
+        if (_vlcInputItemNode->p_item) {
+            _inputItem = [[VLCInputItem alloc] initWithInputItem:_vlcInputItemNode->p_item];
+        }
+    }
+    return self;
+}
+
+- (NSString *)description
+{
+    NSString *inputItemName;
+    if (_vlcInputItemNode && _vlcInputItemNode->p_item)
+        inputItemName = toNSStr(_vlcInputItemNode->p_item->psz_name);
+    else
+        inputItemName = @"p_item == nil";
+    return [NSString stringWithFormat:@"%@: node: %p input name: %@, number of children: %i", NSStringFromClass([self class]),_vlcInputItemNode, inputItemName, self.numberOfChildren];
+}
+
+- (void)clearChildrenCache
+{
+    _cachedChildren = nil;
+}
+
+- (int)numberOfChildren
+{
+    if (_cachedChildren) {
+        return (int)_cachedChildren.count;
+    }
+    return _vlcInputItemNode ? _vlcInputItemNode->i_children : 0;
+}
+
+- (nullable NSArray<VLCInputNode *> *)children
+{
+    if (_cachedChildren) {
+        return _cachedChildren;
+    }
+
+    if (_vlcInputItemNode == NULL) {
+        return nil;
+    }
+    NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:_vlcInputItemNode->i_children];
+    for (int i = 0; i < _vlcInputItemNode->i_children; i++) {
+        if (_vlcInputItemNode->pp_children == NULL) {
+            break;
+        }
+        VLCInputNode *inputNode = [[VLCInputNode alloc] initWithInputNode:_vlcInputItemNode->pp_children[i]];
+        if (inputNode) {
+            [mutableArray addObject:inputNode];
+        }
+    }
+    _cachedChildren = [mutableArray copy];
+    return _cachedChildren;
+}
+
+ at end


=====================================
modules/gui/macosx/library/VLCInputNodePathControlItem.m
=====================================
@@ -23,6 +23,7 @@
 #import "VLCInputNodePathControlItem.h"
 
 #import "VLCInputItem.h"
+#import "VLCInputNode.h"
 #import "VLCLibraryImageCache.h"
 
 #import "extensions/NSString+Helpers.h"


=====================================
modules/gui/macosx/library/VLCLibrarySegment.m
=====================================
@@ -28,6 +28,7 @@
 #import "extensions/NSString+Helpers.h"
 
 #import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
 #import "library/VLCLibraryController.h"
 #import "library/VLCLibraryDataTypes.h"
 #import "library/VLCLibraryModel.h"


=====================================
modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationStack.m
=====================================
@@ -25,6 +25,7 @@
 #import "extensions/NSString+Helpers.h"
 
 #import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
 #import "library/VLCInputNodePathControl.h"
 #import "library/VLCInputNodePathControlItem.h"
 #import "library/VLCLibraryWindow.h"


=====================================
modules/gui/macosx/library/media-source/VLCLocalMediaSource.m
=====================================
@@ -23,6 +23,7 @@
 #import "VLCLocalMediaSource.h"
 
 #import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
 
 #import "VLCLocalMediaSourceNodeObservation.h"
 


=====================================
modules/gui/macosx/library/media-source/VLCMediaSource.m
=====================================
@@ -25,6 +25,7 @@
 
 #import "extensions/NSString+Helpers.h"
 #import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
 
 @interface VLCMediaSource ()
 {


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
=====================================
@@ -36,6 +36,7 @@
 #import "extensions/NSWindow+VLCAdditions.h"
 
 #import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
 #import "library/VLCInputNodePathControl.h"
 #import "library/VLCInputNodePathControlItem.h"
 #import "library/VLCLibraryCollectionViewSupplementaryElementView.h"


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
=====================================
@@ -31,6 +31,7 @@
 #import "extensions/NSTableCellView+VLCAdditions.h"
 
 #import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
 #import "library/VLCInputNodePathControl.h"
 #import "library/VLCInputNodePathControlItem.h"
 #import "library/VLCLibraryImageCache.h"


=====================================
modules/gui/macosx/tests/Makefile.am
=====================================
@@ -9,8 +9,23 @@ check_PROGRAMS += VLCMacOSXTests
 TESTS += $(macosx_gui_xctest_bundles)
 
 VLCMacOSXTests_SOURCES = \
+	gui/macosx/tests/VLCHexNumberFormatterTest.m \
+	gui/macosx/tests/NSStringHelpersTest.m \
+	gui/macosx/tests/VLCInputItemTest.m \
+	gui/macosx/tests/VLCInputItemTestSupport.m \
+	gui/macosx/tests/VLCInputNodeTest.m \
+	gui/macosx/tests/VLCLogMessageTest.m \
 	gui/macosx/tests/VLCPositionFormatterTest.m \
-	gui/macosx/extensions/VLCPositionFormatter.m
+	gui/macosx/tests/VLCTimeFormatterTest.m \
+	gui/macosx/extensions/VLCHexNumberFormatter.m \
+	gui/macosx/extensions/VLCPositionFormatter.m \
+	gui/macosx/extensions/NSString+Helpers.m \
+	gui/macosx/library/VLCInputItem.m \
+	gui/macosx/library/VLCInputNode.m \
+	gui/macosx/windows/logging/VLCLogMessage.m \
+	gui/macosx/views/VLCTimeFormatter.m
+
+noinst_HEADERS += gui/macosx/tests/VLCInputItemTestSupport.h
 
 VLCMacOSXTests_CPPFLAGS = \
 	$(AM_CPPFLAGS) \
@@ -28,10 +43,18 @@ VLCMacOSXTests_LDFLAGS = \
 	-bundle \
 	-F"$(macosx_gui_xctest_frameworks_dir)" \
 	-Wl,-rpath,"$(macosx_gui_xctest_frameworks_dir)" \
+	-Wl,-rpath,"$(abs_top_builddir)/src/.libs" \
 	-Wl,-framework,XCTest \
-	-Wl,-framework,Foundation
+	-Wl,-framework,Foundation \
+	-Wl,-framework,Cocoa \
+	-Wl,-framework,IOKit
+
+VLCMacOSXTests_LDADD = $(top_builddir)/src/libvlccore.la
 
-VLCMacOSXTests_LDADD =
+VLCMacOSXTests_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+	$(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(OBJCLD) \
+	$(VLCMacOSXTests_OBJCFLAGS) $(OBJCFLAGS) \
+	$(VLCMacOSXTests_LDFLAGS) $(LDFLAGS) -o $@
 
 VLCMacOSXTests.xctest: VLCMacOSXTests$(EXEEXT) $(srcdir)/gui/macosx/tests/VLCMacOSXTests-Info.plist
 	$(AM_V_GEN)rm -rf $@


=====================================
modules/gui/macosx/tests/NSStringHelpersTest.m
=====================================
@@ -0,0 +1,352 @@
+/****************************************************************************
+ * NSStringHelpersTest.m: NSString helper native tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+#import <vlc_tick.h>
+
+#import <string.h>
+
+#import "extensions/NSString+Helpers.h"
+#import <vlc_actions.h>
+
+ at interface NSStringHelpersTest : XCTestCase
+ at end
+
+ at implementation NSStringHelpersTest
+
+- (void)testNilSafeStringComparison
+{
+    XCTAssertTrue([NSString equalOrBothNil:nil to:nil]);
+    XCTAssertFalse([NSString equalOrBothNil:nil to:@"value"]);
+    XCTAssertFalse([NSString equalOrBothNil:@"value" to:nil]);
+    XCTAssertTrue([NSString equalOrBothNil:@"value" to:@"value"]);
+    XCTAssertFalse([NSString equalOrBothNil:@"value" to:@"other"]);
+}
+
+- (void)assertVLCKey:(const char *)key equalsFunctionKey:(unichar)functionKey
+{
+    NSString * const expected = [NSString stringWithFormat:@"%C", functionKey];
+    XCTAssertEqualObjects(VLCKeyToString((char *)key), expected);
+}
+
+- (void)testTimeInSecondsFromStringWithColons
+{
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"1:02:03"], 3723);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"02:03"], 123);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"83"], 83);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"00:00:00"], 0);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"1:"], 60);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@":1"], 1);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@":"], 0);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@" 2 : 03"], 123);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"12seconds"], 12);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"1:2:3:4"], 1);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@""], 0);
+    XCTAssertEqual([NSString timeInSecondsFromStringWithColons:nil], 0);
+}
+
+- (void)testTimeFormatting
+{
+    XCTAssertEqualObjects([NSString stringWithTime:0], @"--:--");
+    XCTAssertEqualObjects([NSString stringWithTime:-1], @"--:--");
+    XCTAssertEqualObjects([NSString stringWithTime:1], @"00:01");
+    XCTAssertEqualObjects([NSString stringWithTime:59], @"00:59");
+    XCTAssertEqualObjects([NSString stringWithTime:60], @"01:00");
+    XCTAssertEqualObjects([NSString stringWithTime:3599], @"59:59");
+    XCTAssertEqualObjects([NSString stringWithTime:3600], @"1:00:00");
+    XCTAssertEqualObjects([NSString stringWithTime:86400], @"24:00:00");
+
+    XCTAssertEqualObjects([NSString stringWithTimeFromTicks:VLC_TICK_0], @"00:00");
+    XCTAssertEqualObjects([NSString stringWithTimeFromTicks:VLC_TICK_FROM_SEC(1)], @"00:01");
+    XCTAssertEqualObjects([NSString stringWithTimeFromTicks:VLC_TICK_FROM_SEC(60)], @"01:00");
+    XCTAssertEqualObjects([NSString stringWithTimeFromTicks:VLC_TICK_FROM_SEC(3600)], @"1:00:00");
+    XCTAssertEqualObjects([NSString stringWithTimeFromTicks:-VLC_TICK_FROM_SEC(1)], @"-00:01");
+
+    XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:-1], @"--:--");
+    XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:0], @"--:--");
+    XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:1], @"00:00");
+    XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:999], @"00:00");
+    XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:1000], @"00:01");
+    XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:60000], @"01:00");
+    XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:3600000], @"1:00:00");
+}
+
+- (void)testDurationFormatting
+{
+    XCTAssertEqualObjects([NSString stringWithDuration:VLC_TICK_FROM_SEC(3600)
+                                            currentTime:VLC_TICK_FROM_SEC(60)
+                                               negative:YES], @"-59:00");
+    XCTAssertEqualObjects([NSString stringWithDuration:VLC_TICK_FROM_SEC(3600)
+                                            currentTime:VLC_TICK_FROM_SEC(60)
+                                               negative:NO], @"01:00");
+    XCTAssertEqualObjects([NSString stringWithDuration:VLC_TICK_FROM_SEC(60)
+                                            currentTime:VLC_TICK_FROM_SEC(60)
+                                               negative:YES], @"-00:00");
+    XCTAssertEqualObjects([NSString stringWithDuration:VLC_TICK_FROM_SEC(60)
+                                            currentTime:VLC_TICK_FROM_SEC(90)
+                                               negative:YES], @"-00:00");
+    XCTAssertEqualObjects([NSString stringWithDuration:0
+                                            currentTime:VLC_TICK_FROM_SEC(60)
+                                               negative:YES], @"01:00");
+}
+
+- (void)testExtensionsArrayFromVLCStyleString
+{
+    XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"*.mp4;*.mkv"],
+                          (@[ @"mp4", @"mkv" ]));
+    XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"srt;ass"],
+                          (@[ @"srt", @"ass" ]));
+    XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:NULL], @[]);
+    XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:""], (@[ @"" ]));
+    XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"mp4;"], (@[ @"mp4", @"" ]));
+    XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:";mp4"], (@[ @"", @"mp4" ]));
+    XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"mp4;;mkv"],
+                          (@[ @"mp4", @"", @"mkv" ]));
+    XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"*.tar.gz;*.M4V"],
+                          (@[ @"tar.gz", @"M4V" ]));
+
+    const char invalidUTF8[] = { (char)0xff, '\0' };
+    XCTAssertNil([NSString extensionsArrayFromVLCStyleString:invalidUTF8]);
+}
+
+- (void)testStringWithIncrementedTrailingNumber
+{
+    XCTAssertEqualObjects([@"Duplicate" stringWithIncrementedTrailingNumber], @"Duplicate (2)");
+    XCTAssertEqualObjects([@"Duplicate (2)" stringWithIncrementedTrailingNumber], @"Duplicate (3)");
+    XCTAssertEqualObjects([@"Duplicate (09)" stringWithIncrementedTrailingNumber],
+                          @"Duplicate (10)");
+    XCTAssertEqualObjects([@"" stringWithIncrementedTrailingNumber], @" (2)");
+    XCTAssertEqualObjects([@"Duplicate(2)" stringWithIncrementedTrailingNumber], @"Duplicate(3)");
+    XCTAssertEqualObjects([@"Duplicate (0)" stringWithIncrementedTrailingNumber], @"Duplicate (1)");
+    XCTAssertEqualObjects([@"Duplicate (00)" stringWithIncrementedTrailingNumber],
+                          @"Duplicate (1)");
+    XCTAssertEqualObjects([@"Duplicate (2) (3)" stringWithIncrementedTrailingNumber],
+                          @"Duplicate (2) (4)");
+    XCTAssertEqualObjects([@"Duplicate (2) " stringWithIncrementedTrailingNumber],
+                          @"Duplicate (2)  (2)");
+    XCTAssertEqualObjects([@"Duplicate (x)" stringWithIncrementedTrailingNumber],
+                          @"Duplicate (x) (2)");
+}
+
+- (void)testFlagEmojiStringForCountryCode
+{
+    XCTAssertEqualObjects(flagEmojiStringForCountryCode(@"us"), @"🇺🇸");
+    XCTAssertEqualObjects(flagEmojiStringForCountryCode(@"HK"), @"🇭🇰");
+    XCTAssertEqualObjects(flagEmojiStringForCountryCode(@"aA"), @"🇦🇦");
+    XCTAssertEqualObjects(flagEmojiStringForCountryCode(@"ZZ"), @"🇿🇿");
+    XCTAssertNil(flagEmojiStringForCountryCode(@"U1"));
+    XCTAssertNil(flagEmojiStringForCountryCode(@""));
+    XCTAssertNil(flagEmojiStringForCountryCode(@"U"));
+    XCTAssertNil(flagEmojiStringForCountryCode(@"USA"));
+    XCTAssertNil(flagEmojiStringForCountryCode(@"U-") );
+    XCTAssertNil(flagEmojiStringForCountryCode(@"éé"));
+    XCTAssertNil(flagEmojiStringForCountryCode(nil));
+
+    for (unichar first = 'A'; first <= 'Z'; first++) {
+        for (unichar second = 'A'; second <= 'Z'; second++) {
+            NSString * const code = [NSString stringWithFormat:@"%C%C", first, second];
+            NSString * const flag = flagEmojiStringForCountryCode(code);
+            XCTAssertNotNil(flag, @"Expected a flag for %@", code);
+            XCTAssertEqual(flag.length, (NSUInteger)4, @"Unexpected UTF-16 length for %@", code);
+        }
+    }
+}
+
+- (void)testBase64EncodingAndDecoding
+{
+    XCTAssertNil([NSString base64StringWithCString:NULL]);
+    XCTAssertEqualObjects([NSString base64StringWithCString:""], @"");
+    XCTAssertEqualObjects([NSString base64StringWithCString:"VLC"], @"VkxD");
+    XCTAssertEqualObjects([@"VLC" base64EncodedString], @"VkxD");
+    XCTAssertEqualObjects([@"" base64EncodedString], @"");
+    XCTAssertEqualObjects([@"😀" base64EncodedString], @"8J+YgA==");
+
+    XCTAssertEqualObjects([@"" base64DecodedString], @"");
+    XCTAssertEqualObjects([@"VkxD" base64DecodedString], @"VLC");
+    XCTAssertEqualObjects([@"8J+YgA==" base64DecodedString], @"😀");
+    XCTAssertEqualObjects([@"!" base64DecodedString], @"");
+    XCTAssertEqualObjects([@"YQ$=" base64DecodedString], @"a");
+
+    const char * const invalidUTF8Base64 = "/w==";
+    XCTAssertNil([@(invalidUTF8Base64) base64DecodedString]);
+    XCTAssertEqualObjects(B64DecNSStr(@"!"), @"");
+
+    char * const input = strdup("VLC");
+    XCTAssertEqualObjects(B64EncAndFree(input), @"VkxD");
+}
+
+- (void)testStringWrapping
+{
+    XCTAssertEqualObjects([@"" stringWrappedToWidth:40], @"");
+
+    NSString * const shortString = @"Short text";
+    XCTAssertEqualObjects([shortString stringWrappedToWidth:1000], shortString);
+    XCTAssertEqualObjects(shortString, @"Short text");
+
+    NSString * const longString =
+        @"This is a long sentence that should wrap across multiple lines.";
+    NSString * const wrappedString = [longString stringWrappedToWidth:40];
+    XCTAssertTrue([wrappedString containsString:@"\n"]);
+    XCTAssertEqualObjects([wrappedString stringByReplacingOccurrencesOfString:@"\n" withString:@""],
+                          longString);
+
+    NSString * const multilineString = @"first\nsecond";
+    XCTAssertTrue([[multilineString stringWrappedToWidth:1000] containsString:@"\n"]);
+}
+
+- (void)testCStringConversion
+{
+    XCTAssertEqualObjects(toNSStr(NULL), @"");
+    XCTAssertEqualObjects(toNSStr("VLC"), @"VLC");
+    XCTAssertEqualObjects(toNSStr("caf\xc3\xa9"), @"café");
+
+    const char invalidUTF8[] = { (char)0xff, '\0' };
+    XCTAssertNil(toNSStr(invalidUTF8));
+}
+
+- (void)testCocoaKeyToVLC
+{
+    XCTAssertEqual(CocoaKeyToVLC(NSUpArrowFunctionKey), (unsigned int)KEY_UP);
+    XCTAssertEqual(CocoaKeyToVLC(NSDownArrowFunctionKey), (unsigned int)KEY_DOWN);
+    XCTAssertEqual(CocoaKeyToVLC(NSLeftArrowFunctionKey), (unsigned int)KEY_LEFT);
+    XCTAssertEqual(CocoaKeyToVLC(NSRightArrowFunctionKey), (unsigned int)KEY_RIGHT);
+    XCTAssertEqual(CocoaKeyToVLC(NSInsertFunctionKey), (unsigned int)KEY_INSERT);
+    XCTAssertEqual(CocoaKeyToVLC(NSHomeFunctionKey), (unsigned int)KEY_HOME);
+    XCTAssertEqual(CocoaKeyToVLC(NSEndFunctionKey), (unsigned int)KEY_END);
+    XCTAssertEqual(CocoaKeyToVLC(NSPageUpFunctionKey), (unsigned int)KEY_PAGEUP);
+    XCTAssertEqual(CocoaKeyToVLC(NSPageDownFunctionKey), (unsigned int)KEY_PAGEDOWN);
+    XCTAssertEqual(CocoaKeyToVLC(NSMenuFunctionKey), (unsigned int)KEY_MENU);
+    XCTAssertEqual(CocoaKeyToVLC(NSTabCharacter), (unsigned int)KEY_TAB);
+    XCTAssertEqual(CocoaKeyToVLC(NSCarriageReturnCharacter), (unsigned int)KEY_ENTER);
+    XCTAssertEqual(CocoaKeyToVLC(NSEnterCharacter), (unsigned int)KEY_ENTER);
+    XCTAssertEqual(CocoaKeyToVLC(NSBackspaceCharacter), (unsigned int)KEY_BACKSPACE);
+    XCTAssertEqual(CocoaKeyToVLC(NSDeleteCharacter), (unsigned int)KEY_DELETE);
+
+    NSArray<NSNumber *> * const functionKeys = @[
+        @(NSF1FunctionKey), @(NSF2FunctionKey), @(NSF3FunctionKey), @(NSF4FunctionKey),
+        @(NSF5FunctionKey), @(NSF6FunctionKey), @(NSF7FunctionKey), @(NSF8FunctionKey),
+        @(NSF9FunctionKey), @(NSF10FunctionKey), @(NSF11FunctionKey), @(NSF12FunctionKey)
+    ];
+    for (NSUInteger index = 0; index < functionKeys.count; index++) {
+        XCTAssertEqual(CocoaKeyToVLC(functionKeys[index].unsignedShortValue),
+                       (unsigned int)(KEY_F1 + (index << 16)));
+    }
+
+    XCTAssertEqual(CocoaKeyToVLC('A'), (unsigned int)'A');
+    XCTAssertEqual(CocoaKeyToVLC(0), 0U);
+}
+
+- (void)testOSXStringKeyToString
+{
+    XCTAssertEqualObjects(OSXStringKeyToString(@"a"), @"A");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Command+Shift+P"), @"⌘⇧P");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Command+"), @"⌘+");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Ctrl-"), @"⌃-");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Right"), @"→");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Left"), @"←");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Page Up"), @"⇞");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Page Down"), @"⇟");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Up"), @"↑");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Down"), @"↓");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Enter"), @"↵");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Tab"), @"⇥");
+    XCTAssertEqualObjects(OSXStringKeyToString(@"Delete"), @"⌫");
+    XCTAssertTrue([OSXStringKeyToString(@"") length] > 0);
+}
+
+- (void)testVLCKeyToString
+{
+    XCTAssertEqualObjects(VLCKeyToString(NULL), @"");
+    XCTAssertEqualObjects(VLCKeyToString((char *)""), @"");
+    XCTAssertEqualObjects(VLCKeyToString((char *)"a"), @"a");
+    XCTAssertEqualObjects(VLCKeyToString((char *)"Command+Shift+a"), @"a");
+    XCTAssertEqualObjects(VLCKeyToString((char *)"Command+"), @"+");
+    [self assertVLCKey:"Page Up" equalsFunctionKey:NSPageUpFunctionKey];
+    [self assertVLCKey:"Page Down" equalsFunctionKey:NSPageDownFunctionKey];
+    [self assertVLCKey:"Up" equalsFunctionKey:NSUpArrowFunctionKey];
+    [self assertVLCKey:"Down" equalsFunctionKey:NSDownArrowFunctionKey];
+    [self assertVLCKey:"Right" equalsFunctionKey:NSRightArrowFunctionKey];
+    [self assertVLCKey:"Left" equalsFunctionKey:NSLeftArrowFunctionKey];
+    [self assertVLCKey:"Enter" equalsFunctionKey:NSEnterCharacter];
+    [self assertVLCKey:"Insert" equalsFunctionKey:NSInsertFunctionKey];
+    [self assertVLCKey:"Home" equalsFunctionKey:NSHomeFunctionKey];
+    [self assertVLCKey:"End" equalsFunctionKey:NSEndFunctionKey];
+    [self assertVLCKey:"Menu" equalsFunctionKey:NSMenuFunctionKey];
+    [self assertVLCKey:"Tab" equalsFunctionKey:NSTabCharacter];
+    [self assertVLCKey:"Backspace" equalsFunctionKey:NSBackspaceCharacter];
+    [self assertVLCKey:"Delete" equalsFunctionKey:NSDeleteCharacter];
+    [self assertVLCKey:"F12" equalsFunctionKey:NSF12FunctionKey];
+    [self assertVLCKey:"F1" equalsFunctionKey:NSF1FunctionKey];
+    for (NSUInteger functionNumber = 2; functionNumber <= 11; functionNumber++) {
+        NSString * const key = [NSString stringWithFormat:@"F%lu", functionNumber];
+        NSString * const expected = [NSString stringWithFormat:@"%C",
+                                      (unichar)(NSF1FunctionKey + functionNumber - 1)];
+        XCTAssertEqualObjects(VLCKeyToString((char *)key.UTF8String), expected);
+    }
+    XCTAssertEqualObjects(VLCKeyToString((char *)"Space"), @" ");
+    XCTAssertEqualObjects(VLCKeyToString((char *)"Escape"), @"Escape");
+}
+
+- (void)testVLCModifiersToCocoa
+{
+    XCTAssertEqual(VLCModifiersToCocoa((char *)""), 0U);
+    XCTAssertEqual(VLCModifiersToCocoa((char *)"Command"),
+                   (unsigned int)NSEventModifierFlagCommand);
+    XCTAssertEqual(VLCModifiersToCocoa((char *)"Alt"), (unsigned int)NSEventModifierFlagOption);
+    XCTAssertEqual(VLCModifiersToCocoa((char *)"Shift"), (unsigned int)NSEventModifierFlagShift);
+    XCTAssertEqual(VLCModifiersToCocoa((char *)"Ctrl"), (unsigned int)NSEventModifierFlagControl);
+    XCTAssertEqual(VLCModifiersToCocoa((char *)"CommandAltShiftCtrl"),
+                   (unsigned int)(NSEventModifierFlagCommand |
+                                  NSEventModifierFlagOption |
+                                  NSEventModifierFlagShift |
+                                  NSEventModifierFlagControl));
+    XCTAssertEqual(VLCModifiersToCocoa((char *)"Unknown"), 0U);
+}
+
+- (void)testVolumeHelpersForTemporaryDirectories
+{
+    NSString * const uuid = [NSUUID UUID].UUIDString;
+    NSString * const path = [NSTemporaryDirectory() stringByAppendingPathComponent:uuid];
+    NSFileManager * const fileManager = [[NSFileManager alloc] init];
+    XCTAssertTrue([fileManager createDirectoryAtPath:path
+                         withIntermediateDirectories:NO
+                                          attributes:nil
+                                               error:nil]);
+
+    XCTAssertEqualObjects(getVolumeTypeFromMountPath(path), kVLCMediaVideoTSFolder);
+
+    NSString * const vcdPath = [path stringByAppendingPathComponent:kVLCMediaVCD];
+    XCTAssertTrue([fileManager createDirectoryAtPath:vcdPath
+                         withIntermediateDirectories:NO
+                                          attributes:nil
+                                               error:nil]);
+    XCTAssertEqualObjects(getVolumeTypeFromMountPath(path), kVLCMediaVCD);
+
+    [fileManager removeItemAtPath:vcdPath error:nil];
+    NSString * const bdmvPath = [path stringByAppendingPathComponent:kVLCMediaBDMVFolder];
+    XCTAssertTrue([fileManager createDirectoryAtPath:bdmvPath
+                         withIntermediateDirectories:NO
+                                          attributes:nil
+                                               error:nil]);
+    XCTAssertEqualObjects(getVolumeTypeFromMountPath(path), kVLCMediaBDMVFolder);
+
+    NSString * const missingPath = [path stringByAppendingPathComponent:@"missing"];
+    XCTAssertEqualObjects(getVolumeTypeFromMountPath(nil), @"");
+    XCTAssertEqualObjects(getVolumeTypeFromMountPath(missingPath), @"");
+    XCTAssertEqualObjects(getBSDNodeFromMountPath(nil), @"");
+    XCTAssertEqualObjects(getBSDNodeFromMountPath(missingPath), @"");
+
+    [fileManager removeItemAtPath:path error:nil];
+}
+
+ at end


=====================================
modules/gui/macosx/tests/VLCHexNumberFormatterTest.m
=====================================
@@ -0,0 +1,50 @@
+/****************************************************************************
+ * VLCHexNumberFormatterTest.m: VLCHexNumberFormatter native tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+
+#import "extensions/VLCHexNumberFormatter.h"
+
+ at interface VLCHexNumberFormatterTest : XCTestCase
+ at end
+
+ at implementation VLCHexNumberFormatterTest
+
+- (void)testStringForObjectValue
+{
+    VLCHexNumberFormatter *formatter = [[VLCHexNumberFormatter alloc] init];
+
+    XCTAssertEqualObjects([formatter stringForObjectValue:@0], @"000000");
+    XCTAssertEqualObjects([formatter stringForObjectValue:@(0x12AB)], @"0012AB");
+    XCTAssertNil([formatter stringForObjectValue:@"12AB"]);
+    XCTAssertNil([formatter stringForObjectValue:nil]);
+}
+
+- (void)testGetObjectValue
+{
+    VLCHexNumberFormatter *formatter = [[VLCHexNumberFormatter alloc] init];
+    id object = nil;
+
+    XCTAssertTrue([formatter getObjectValue:&object
+                                  forString:@"0012AB"
+                           errorDescription:nil]);
+    XCTAssertEqualObjects(object, @(0x12AB));
+
+    object = nil;
+    XCTAssertFalse([formatter getObjectValue:&object
+                                   forString:@"not-hex"
+                            errorDescription:nil]);
+    XCTAssertNil(object);
+}
+
+ at end


=====================================
modules/gui/macosx/tests/VLCInputItemTest.m
=====================================
@@ -0,0 +1,489 @@
+/****************************************************************************
+ * VLCInputItemTest.m: VLC input item native tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+
+#import "library/VLCInputItem.h"
+#import "tests/VLCInputItemTestSupport.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+
+ at interface VLCInputItem (VLCInputItemTestPrivate)
+- (void)parsingEnded:(int)status;
+- (void)subTreeAdded:(input_item_node_t *)p_node;
+ at end
+
+ at interface VLCInputItemTest : XCTestCase
+ at end
+
+ at implementation VLCInputItemTest
+
+- (input_item_t *)newFileInputItem
+{
+    return input_item_NewFile("file:///tmp/VLC%20Test.mp4",
+                              "Original Name",
+                              VLC_TICK_FROM_SEC(123),
+                              ITEM_LOCAL);
+}
+
+- (void)testInitializationMapsCoreProperties
+{
+    input_item_t * const coreItem = [self newFileInputItem];
+    XCTAssertNotEqual(coreItem, NULL);
+
+    input_item_SetTitle(coreItem, "Metadata Title");
+    input_item_SetArtist(coreItem, "Artist");
+    input_item_SetAlbum(coreItem, "Album");
+    input_item_SetTrackNumber(coreItem, "4");
+    input_item_SetGenre(coreItem, "Genre");
+    input_item_SetCopyright(coreItem, "Copyright");
+    input_item_SetPublisher(coreItem, "Publisher");
+    input_item_SetLanguage(coreItem, "en");
+    input_item_SetDate(coreItem, "2026");
+    input_item_SetDescription(coreItem, "Description");
+    input_item_SetEncodedBy(coreItem, "Encoder");
+    input_item_SetTrackID(coreItem, "track-id");
+    input_item_SetTrackTotal(coreItem, "10");
+    input_item_SetDirector(coreItem, "Director");
+    input_item_SetSeason(coreItem, "2");
+    input_item_SetEpisode(coreItem, "3");
+    input_item_SetShowName(coreItem, "Show");
+    input_item_SetActors(coreItem, "Actor");
+    input_item_SetDiscNumber(coreItem, "1");
+    input_item_SetDiscTotal(coreItem, "2");
+    input_item_SetMetaExtra(coreItem, "custom", "value");
+    input_item_AddOption(coreItem, "network-caching=1000", 0);
+    input_item_AddOption(coreItem, "http-referrer=https://example.com", 0);
+    input_item_SetArtworkURL(coreItem, "https://example.com/art.jpg");
+
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    XCTAssertNotNil(inputItem);
+    XCTAssertEqualObjects(inputItem.MRL, @"file:///tmp/VLC%20Test.mp4");
+    XCTAssertEqualObjects(inputItem.decodedMRL, @"file:///tmp/VLC Test.mp4");
+    XCTAssertEqualObjects(inputItem.path, @"/tmp/VLC Test.mp4");
+    XCTAssertEqualObjects(inputItem.name, @"Original Name");
+    XCTAssertEqualObjects(inputItem.title, @"Metadata Title");
+    XCTAssertEqualObjects(inputItem.artist, @"Artist");
+    XCTAssertEqualObjects(inputItem.album, @"Album");
+    XCTAssertEqualObjects(inputItem.trackNumber, @"4");
+    XCTAssertEqualObjects(inputItem.genre, @"Genre");
+    XCTAssertEqualObjects(inputItem.copyright, @"Copyright");
+    XCTAssertEqualObjects(inputItem.publisher, @"Publisher");
+    XCTAssertEqualObjects(inputItem.language, @"en");
+    XCTAssertEqualObjects(inputItem.date, @"2026");
+    XCTAssertEqualObjects(inputItem.contentDescription, @"Description");
+    XCTAssertEqualObjects(inputItem.encodedBy, @"Encoder");
+    XCTAssertEqualObjects(inputItem.trackID, @"track-id");
+    XCTAssertEqualObjects(inputItem.trackTotal, @"10");
+    XCTAssertEqualObjects(inputItem.director, @"Director");
+    XCTAssertEqualObjects(inputItem.season, @"2");
+    XCTAssertEqualObjects(inputItem.episode, @"3");
+    XCTAssertEqualObjects(inputItem.showName, @"Show");
+    XCTAssertEqualObjects(inputItem.actors, @"Actor");
+    XCTAssertEqualObjects(inputItem.discNumber, @"1");
+    XCTAssertEqualObjects(inputItem.totalNumberOfDiscs, @"2");
+    XCTAssertEqual(inputItem.duration, VLC_TICK_FROM_SEC(123));
+    XCTAssertEqual(inputItem.inputType, ITEM_TYPE_FILE);
+    XCTAssertFalse(inputItem.isStream);
+    XCTAssertFalse(inputItem.preparsed);
+    XCTAssertEqualObjects(inputItem.options,
+                          (@[ @"network-caching=1000", @"http-referrer=https://example.com" ]));
+    XCTAssertEqualObjects([inputItem extraMetaForKey:@"custom"], @"value");
+    XCTAssertEqualObjects(inputItem.extraMetaNames, (@[ @"custom" ]));
+    XCTAssertEqualObjects(inputItem.artworkURL.absoluteString, @"https://example.com/art.jpg");
+    XCTAssertNil(inputItem.finderTags);
+}
+
+- (void)testMetadataSettersUpdateCoreItem
+{
+    input_item_t * const coreItem = [self newFileInputItem];
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    inputItem.name = @"New Name";
+    inputItem.title = @"New Title";
+    inputItem.artist = @"New Artist";
+    inputItem.album = @"New Album";
+    inputItem.trackNumber = @"8";
+    inputItem.genre = @"New Genre";
+    inputItem.copyright = @"New Copyright";
+    inputItem.publisher = @"New Publisher";
+    inputItem.language = @"fr";
+    inputItem.date = @"2027";
+    inputItem.contentDescription = @"New Description";
+    inputItem.director = @"New Director";
+    inputItem.showName = @"New Show";
+    inputItem.actors = @"New Actor";
+
+    XCTAssertEqualObjects(inputItem.name, @"New Name");
+    XCTAssertEqualObjects(inputItem.title, @"New Title");
+    XCTAssertEqualObjects(inputItem.artist, @"New Artist");
+    XCTAssertEqualObjects(inputItem.album, @"New Album");
+    XCTAssertEqualObjects(inputItem.trackNumber, @"8");
+    XCTAssertEqualObjects(inputItem.genre, @"New Genre");
+    XCTAssertEqualObjects(inputItem.copyright, @"New Copyright");
+    XCTAssertEqualObjects(inputItem.publisher, @"New Publisher");
+    XCTAssertEqualObjects(inputItem.language, @"fr");
+    XCTAssertEqualObjects(inputItem.date, @"2027");
+    XCTAssertEqualObjects(inputItem.contentDescription, @"New Description");
+    XCTAssertEqualObjects(inputItem.director, @"New Director");
+    XCTAssertEqualObjects(inputItem.showName, @"New Show");
+    XCTAssertEqualObjects(inputItem.actors, @"New Actor");
+}
+
+- (void)testDefaultValuesAndTitleFallback
+{
+    input_item_t * const coreItem = [self newFileInputItem];
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    XCTAssertEqualObjects(inputItem.title, @"Original Name");
+    XCTAssertEqualObjects(inputItem.options, @[]);
+    XCTAssertEqualObjects(inputItem.extraMetaNames, @[]);
+    XCTAssertEqualObjects([inputItem extraMetaForKey:@"missing"], @"");
+    XCTAssertNil(inputItem.finderTags);
+}
+
+- (void)testCancelParsingWithoutParser
+{
+    input_item_t * const coreItem = [self newFileInputItem];
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    XCTAssertNoThrow([inputItem cancelParsing]);
+}
+
+- (void)testParsingEndedPostsSuccessAndFailureNotifications
+{
+    input_item_t * const coreItem = [self newFileInputItem];
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    NSMutableArray<NSString *> * const notificationNames = [NSMutableArray array];
+    id const observer = [NSNotificationCenter.defaultCenter
+        addObserverForName:nil
+                    object:inputItem
+                     queue:nil
+                usingBlock:^(NSNotification * const notification) {
+        [notificationNames addObject:notification.name];
+    }];
+
+    [inputItem parsingEnded:1];
+    [inputItem parsingEnded:0];
+
+    [NSNotificationCenter.defaultCenter removeObserver:observer];
+
+    XCTAssertEqual(notificationNames.count, (NSUInteger)2);
+    XCTAssertTrue([notificationNames[0] isEqualToString:VLCInputItemParsingSucceeded]);
+    XCTAssertTrue([notificationNames[1] isEqualToString:VLCInputItemParsingFailed]);
+}
+
+- (void)testSubtreeReplacementPostsNotifications
+{
+    input_item_t * const coreItem = [self newFileInputItem];
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    input_item_node_t * const firstSubtree = input_item_node_Create(inputItem.vlcInputItem);
+    input_item_node_t * const secondSubtree = input_item_node_Create(inputItem.vlcInputItem);
+    NSMutableArray<NSNotification *> * const notifications = [NSMutableArray array];
+    id const observer = [NSNotificationCenter.defaultCenter
+        addObserverForName:VLCInputItemSubtreeAdded
+                    object:inputItem
+                     queue:nil
+                usingBlock:^(NSNotification * const notification) {
+        [notifications addObject:notification];
+    }];
+
+    [inputItem subTreeAdded:firstSubtree];
+    XCTAssertEqual(inputItem.subTree, firstSubtree);
+    [inputItem subTreeAdded:secondSubtree];
+    XCTAssertEqual(inputItem.subTree, secondSubtree);
+
+    [NSNotificationCenter.defaultCenter removeObserver:observer];
+
+    XCTAssertEqual(notifications.count, (NSUInteger)2);
+    XCTAssertEqual(notifications[0].object, inputItem);
+    XCTAssertEqual(notifications[1].object, inputItem);
+}
+
+- (void)testURLFactoryAndRadioCountryCode
+{
+    NSURL * const url = [NSURL URLWithString:@"file:///tmp/Example%20Video.mkv"];
+    VLCInputItem * const inputItem = [VLCInputItem inputItemFromURL:url];
+
+    XCTAssertNotNil(inputItem);
+    XCTAssertEqualObjects(inputItem.MRL, @"file:///tmp/Example%20Video.mkv");
+    XCTAssertEqualObjects(inputItem.name, @"Example Video");
+    XCTAssertNil(inputItem.radioCountryCodeForFlagArtwork);
+
+    VLCInputItem * const radioItem =
+        [VLCInputItem inputItemFromURL:[NSURL URLWithString:@"radio://us/station"]];
+    XCTAssertEqualObjects(radioItem.radioCountryCodeForFlagArtwork, @"US");
+
+    VLCInputItem * const invalidRadioItem =
+        [VLCInputItem inputItemFromURL:[NSURL URLWithString:@"radio://u1/station"]];
+    XCTAssertNil(invalidRadioItem.radioCountryCodeForFlagArtwork);
+}
+
+- (void)testStreamAndArtworkState
+{
+    input_item_t * const coreItem = input_item_NewStream("https://example.com/live", 
+                                                        "Live stream",
+                                                        INPUT_DURATION_INDEFINITE);
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    XCTAssertNotNil(inputItem);
+    XCTAssertTrue(inputItem.isStream);
+    XCTAssertEqual(inputItem.inputType, ITEM_TYPE_STREAM);
+    XCTAssertEqualObjects(inputItem.decodedMRL, @"https://example.com/live");
+    XCTAssertEqualObjects(inputItem.path, @"");
+    XCTAssertEqualObjects(inputItem.options, @[]);
+    XCTAssertNil(inputItem.finderTags);
+
+    inputItem.artworkURL = [NSURL URLWithString:@"https://example.com/live.jpg"];
+    XCTAssertEqualObjects(inputItem.artworkURL.absoluteString, @"https://example.com/live.jpg");
+
+    [(id)inputItem setArtworkURL:nil];
+    XCTAssertNil(inputItem.artworkURL);
+}
+
+- (void)testStreamThumbnailCompletesWithoutImage
+{
+    input_item_t * const coreItem = input_item_NewStream("https://example.com/live",
+                                                        "Live stream",
+                                                        INPUT_DURATION_INDEFINITE);
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    XCTestExpectation * const completionExpectation =
+        [self expectationWithDescription:@"stream thumbnail completion"];
+    [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+                completionHandler:^(NSImage *image) {
+        XCTAssertNil(image);
+        [completionExpectation fulfill];
+    }];
+    [self waitForExpectations:@[completionExpectation] timeout:1.0];
+}
+
+- (void)testLocalThumbnailCompletes
+{
+    VLCInputItemTestResetAppKitState();
+
+    NSString * const path = [NSTemporaryDirectory()
+        stringByAppendingPathComponent:@"VLCInputItemThumbnailTest.txt"];
+    NSData * const contents = [@"VLC thumbnail test" dataUsingEncoding:NSUTF8StringEncoding];
+    XCTAssertTrue([contents writeToFile:path atomically:YES]);
+
+    NSURL * const url = [NSURL fileURLWithPath:path];
+    input_item_t * const coreItem = input_item_NewFile(url.absoluteString.UTF8String,
+                                                       "Thumbnail Test",
+                                                       INPUT_DURATION_UNSET,
+                                                       ITEM_LOCAL);
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    NSImage * const testImage = [[NSImage alloc] initWithSize:NSMakeSize(1, 1)];
+    VLCInputItemTestSetWorkspaceImage(testImage);
+    XCTestExpectation * const completionExpectation =
+        [self expectationWithDescription:@"workspace thumbnail completion"];
+    [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+                completionHandler:^(NSImage *image) {
+        XCTAssertEqualObjects(image, testImage);
+        [completionExpectation fulfill];
+    }];
+    [self waitForExpectations:@[completionExpectation] timeout:2.0];
+
+    VLCInputItemTestSetWorkspaceImage(nil);
+    VLCInputItemTestSetQuickLookImage(testImage);
+    XCTestExpectation * const quickLookExpectation =
+        [self expectationWithDescription:@"Quick Look thumbnail completion"];
+    [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+                completionHandler:^(NSImage *image) {
+        XCTAssertEqualObjects(image, testImage);
+        [quickLookExpectation fulfill];
+    }];
+    [self waitForExpectations:@[quickLookExpectation] timeout:2.0];
+
+    VLCInputItemTestSetQuickLookImage(nil);
+    XCTestExpectation * const emptyExpectation =
+        [self expectationWithDescription:@"empty thumbnail completion"];
+    [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+                completionHandler:^(NSImage *image) {
+        XCTAssertNil(image);
+        [emptyExpectation fulfill];
+    }];
+    [self waitForExpectations:@[emptyExpectation] timeout:2.0];
+
+    [NSFileManager.defaultManager removeItemAtPath:path error:nil];
+    VLCInputItemTestResetAppKitState();
+}
+
+- (void)testLocalFileOperationsUseTheirApplicationServices
+{
+    VLCInputItemTestResetAppKitState();
+    VLCInputItem * const inputItem =
+        [VLCInputItem inputItemFromURL:[NSURL fileURLWithPath:@"/tmp/VLCInputItemTest.mkv"]];
+
+    XCTAssertNotNil(inputItem);
+    [inputItem moveToTrash];
+    XCTAssertTrue(VLCInputItemTestDidReload());
+
+    [inputItem revealInFinder];
+    XCTAssertTrue(VLCInputItemTestDidReveal());
+
+    VLCInputItemTestResetAppKitState();
+}
+
+- (void)testMissingURIShortCircuitsLocalOperations
+{
+    input_item_t * const coreItem = input_item_New(NULL, "No URI");
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    XCTAssertNotNil(inputItem);
+    XCTAssertEqualObjects(inputItem.path, @"");
+    XCTAssertNil(inputItem.finderTags);
+
+    XCTestExpectation * const completionExpectation =
+        [self expectationWithDescription:@"missing URI thumbnail completion"];
+    [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+                completionHandler:^(NSImage *image) {
+        XCTAssertNil(image);
+        [completionExpectation fulfill];
+    }];
+    [self waitForExpectations:@[completionExpectation] timeout:1.0];
+}
+
+- (void)testMalformedLocalURIProducesNoFinderTags
+{
+    input_item_t * const coreItem = input_item_NewFile("file://[",
+                                                       "Malformed",
+                                                       INPUT_DURATION_UNSET,
+                                                       ITEM_LOCAL);
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    XCTAssertNotNil(inputItem);
+    XCTAssertEqualObjects(inputItem.path, @"");
+    XCTAssertNil(inputItem.finderTags);
+
+    XCTestExpectation * const completionExpectation =
+        [self expectationWithDescription:@"malformed URI thumbnail completion"];
+    [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+                completionHandler:^(NSImage *image) {
+        XCTAssertNil(image);
+        [completionExpectation fulfill];
+    }];
+    [self waitForExpectations:@[completionExpectation] timeout:1.0];
+}
+
+- (void)testWritingMetadataForStreamFailsWithoutTouchingTheFilesystem
+{
+    input_item_t * const coreItem = input_item_NewStream("https://example.com/live",
+                                                         "Live stream",
+                                                         INPUT_DURATION_INDEFINITE);
+    VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+    input_item_Release(coreItem);
+
+    XCTAssertNotEqual([inputItem writeMetadataToFile], VLC_SUCCESS);
+}
+
+- (void)testCommonInputItemData
+{
+    XCTAssertEqualObjects(commonInputItemData(@[]), @{});
+
+    input_item_t * const firstCoreItem = [self newFileInputItem];
+    VLCInputItem * const firstInputItem = [[VLCInputItem alloc] initWithInputItem:firstCoreItem];
+    input_item_Release(firstCoreItem);
+
+    XCTAssertEqualObjects(commonInputItemData(@[firstInputItem]),
+                          (@{ @"inputItem": firstInputItem }));
+
+    input_item_t * const secondCoreItem = [self newFileInputItem];
+    VLCInputItem * const secondInputItem = [[VLCInputItem alloc] initWithInputItem:secondCoreItem];
+    input_item_Release(secondCoreItem);
+
+    NSDictionary<NSString *, id> *commonData =
+        commonInputItemData(@[ firstInputItem, secondInputItem ]);
+    XCTAssertEqualObjects(commonData[@"MRL"], firstInputItem.MRL);
+    XCTAssertEqualObjects(commonData[@"title"], firstInputItem.title);
+    XCTAssertEqualObjects(commonData[@"artist"], @"");
+    XCTAssertEqualObjects(commonData[@"artworkURL"], @"");
+
+    secondInputItem.artist = @"Different artist";
+    secondInputItem.artworkURL = [NSURL URLWithString:@"https://example.com/art.jpg"];
+    commonData = commonInputItemData(@[ firstInputItem, secondInputItem ]);
+    XCTAssertEqualObjects(commonData[@"artist"], VLCInputItemCommonDataDifferingFlagString);
+    XCTAssertEqualObjects(commonData[@"artworkURL"], VLCInputItemCommonDataDifferingFlagString);
+}
+
+- (void)testCommonInputItemDataMarksEveryDifferingStringProperty
+{
+    input_item_t * const firstCoreItem = [self newFileInputItem];
+    VLCInputItem * const firstInputItem = [[VLCInputItem alloc] initWithInputItem:firstCoreItem];
+    input_item_Release(firstCoreItem);
+
+    input_item_t * const secondCoreItem = input_item_NewFile("file:///tmp/Other.mp4",
+                                                             "Other Name",
+                                                             VLC_TICK_FROM_SEC(123),
+                                                             ITEM_LOCAL);
+    input_item_SetTitle(secondCoreItem, "Other Title");
+    input_item_SetArtist(secondCoreItem, "Other Artist");
+    input_item_SetAlbum(secondCoreItem, "Other Album");
+    input_item_SetTrackNumber(secondCoreItem, "9");
+    input_item_SetTrackTotal(secondCoreItem, "11");
+    input_item_SetGenre(secondCoreItem, "Other Genre");
+    input_item_SetDate(secondCoreItem, "2027");
+    input_item_SetEpisode(secondCoreItem, "4");
+    input_item_SetActors(secondCoreItem, "Other Actor");
+    input_item_SetDirector(secondCoreItem, "Other Director");
+    input_item_SetShowName(secondCoreItem, "Other Show");
+    input_item_SetCopyright(secondCoreItem, "Other Copyright");
+    input_item_SetPublisher(secondCoreItem, "Other Publisher");
+    input_item_SetNowPlaying(secondCoreItem, "Other Now Playing");
+    input_item_SetLanguage(secondCoreItem, "de");
+    input_item_SetDescription(secondCoreItem, "Other Description");
+    input_item_SetEncodedBy(secondCoreItem, "Other Encoder");
+    input_item_SetArtworkURL(secondCoreItem, "https://example.com/other-art.jpg");
+    input_item_SetTrackID(secondCoreItem, "other-track-id");
+    VLCInputItem * const secondInputItem = [[VLCInputItem alloc] initWithInputItem:secondCoreItem];
+    input_item_Release(secondCoreItem);
+
+    NSDictionary<NSString *, id> * const commonData =
+        commonInputItemData(@[ firstInputItem, secondInputItem, secondInputItem ]);
+    NSArray<NSString *> * const differingProperties = @[
+        @"MRL", @"decodedMRL", @"title", @"artist", @"album", @"trackNumber",
+        @"trackTotal", @"genre", @"date", @"episode", @"actors", @"director",
+        @"showName", @"copyright", @"publisher", @"nowPlaying", @"language",
+        @"contentDescription", @"encodedBy", @"artworkURL"
+    ];
+    for (NSString * const property in differingProperties) {
+        XCTAssertEqualObjects(commonData[property], VLCInputItemCommonDataDifferingFlagString,
+                              @"%@ should be marked as differing", property);
+    }
+}
+
+- (void)testRejectsNilInputItem
+{
+    input_item_t * const nilInputItem = NULL;
+    XCTAssertNil([[VLCInputItem alloc] initWithInputItem:nilInputItem]);
+}
+
+ at end
+
+#pragma clang diagnostic pop


=====================================
modules/gui/macosx/tests/VLCInputItemTestSupport.h
=====================================
@@ -0,0 +1,18 @@
+/****************************************************************************
+ * VLCInputItemTestSupport.h: VLC input item test support declarations
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version.
+ ****************************************************************************/
+
+#import <Cocoa/Cocoa.h>
+
+void VLCInputItemTestSetQuickLookImage(NSImage * _Nullable image);
+void VLCInputItemTestSetWorkspaceImage(NSImage * _Nullable image);
+void VLCInputItemTestResetAppKitState(void);
+BOOL VLCInputItemTestDidReveal(void);
+BOOL VLCInputItemTestDidReload(void);


=====================================
modules/gui/macosx/tests/VLCInputItemTestSupport.m
=====================================
@@ -0,0 +1,131 @@
+/****************************************************************************
+ * VLCInputItemTestSupport.m: VLC input item test-only application stubs
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version.
+ *****************************************************************************/
+
+#import <Cocoa/Cocoa.h>
+#import <vlc_interface.h>
+
+#import "VLCInputItemTestSupport.h"
+
+static NSImage *sQuickLookImage;
+static NSImage *sWorkspaceImage;
+static BOOL sDidReveal;
+static BOOL sDidReload;
+
+ at interface VLCLibraryController : NSObject
+ at end
+
+ at interface VLCMain : NSObject
++ (instancetype)sharedInstance;
+ at end
+
+ at implementation VLCLibraryController
+
+- (void)reloadMediaLibraryFoldersForInputItems:(NSArray * __unused)inputItems
+{
+    sDidReload = YES;
+}
+
+ at end
+
+ at implementation VLCMain
+
++ (instancetype)sharedInstance
+{
+    static VLCMain *main;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        main = [[VLCMain alloc] init];
+    });
+    return main;
+}
+
+- (VLCLibraryController *)libraryController
+{
+    static VLCLibraryController *libraryController;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        libraryController = [[VLCLibraryController alloc] init];
+    });
+    return libraryController;
+}
+
+ at end
+
+intf_thread_t *getIntf(void)
+{
+    return NULL;
+}
+
+void VLCInputItemTestSetQuickLookImage(NSImage *image)
+{
+    sQuickLookImage = image;
+}
+
+void VLCInputItemTestSetWorkspaceImage(NSImage *image)
+{
+    sWorkspaceImage = image;
+}
+
+void VLCInputItemTestResetAppKitState(void)
+{
+    sQuickLookImage = nil;
+    sWorkspaceImage = nil;
+    sDidReveal = NO;
+    sDidReload = NO;
+}
+
+BOOL VLCInputItemTestDidReveal(void)
+{
+    return sDidReveal;
+}
+
+BOOL VLCInputItemTestDidReload(void)
+{
+    return sDidReload;
+}
+
+ at interface NSImage (VLCInputItemTestSupport)
++ (void)quickLookPreviewForLocalPath:(NSString *)path
+                             withSize:(NSSize)size
+                    completionHandler:(void (^)(NSImage *image))completionHandler;
+ at end
+
+ at interface NSWorkspace (VLCInputItemTestSupport)
+- (NSImage *)iconForFile:(NSString *)fullPath;
+- (void)activateFileViewerSelectingURLs:(NSArray<NSURL *> *)fileURLs;
+ at end
+
+ at implementation NSWorkspace (VLCInputItemTestSupport)
+
+- (NSImage *)iconForFile:(NSString * __unused)fullPath
+{
+    return sWorkspaceImage;
+}
+
+- (void)activateFileViewerSelectingURLs:(NSArray<NSURL *> * __unused)fileURLs
+{
+    sDidReveal = YES;
+}
+
+ at end
+
+ at implementation NSImage (VLCInputItemTestSupport)
+
++ (void)quickLookPreviewForLocalPath:(NSString * __unused)path
+                             withSize:(NSSize __unused)size
+                    completionHandler:(void (^)(NSImage *image))completionHandler
+{
+    completionHandler(sQuickLookImage);
+}
+
+ at end


=====================================
modules/gui/macosx/tests/VLCInputNodeTest.m
=====================================
@@ -0,0 +1,219 @@
+/****************************************************************************
+ * VLCInputNodeTest.m: VLC input node native tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+
+#import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+
+ at interface VLCInputNodeTest : XCTestCase
+ at end
+
+ at implementation VLCInputNodeTest
+
+- (input_item_node_t *)newRootNodeWithRootItem:(input_item_t **)rootItem
+                                       childOne:(input_item_t **)childOne
+                                       childTwo:(input_item_t **)childTwo
+{
+    *rootItem = input_item_NewDirectory("vlc://root", "Root", ITEM_LOCAL);
+    *childOne = input_item_NewFile("file:///tmp/One.mp4", "One", VLC_TICK_FROM_SEC(1), ITEM_LOCAL);
+    *childTwo = input_item_NewFile("file:///tmp/Two.mp4", "Two", VLC_TICK_FROM_SEC(2), ITEM_LOCAL);
+
+    input_item_node_t * const rootNode = input_item_node_Create(*rootItem);
+    input_item_node_AppendItem(rootNode, *childOne);
+    return rootNode;
+}
+
+- (void)testRootAndChildrenMapToInputItems
+{
+    input_item_t *rootItem = NULL;
+    input_item_t *childOne = NULL;
+    input_item_t *childTwo = NULL;
+    input_item_node_t * const rootNode =
+        [self newRootNodeWithRootItem:&rootItem childOne:&childOne childTwo:&childTwo];
+    input_item_node_AppendItem(rootNode, childTwo);
+
+    VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:rootNode];
+
+    XCTAssertEqual(inputNode.vlcInputItemNode, rootNode);
+    XCTAssertNotNil(inputNode.inputItem);
+    XCTAssertEqualObjects(inputNode.inputItem.name, @"Root");
+    XCTAssertEqual(inputNode.numberOfChildren, 2);
+
+    NSArray<VLCInputNode *> * const children = inputNode.children;
+    XCTAssertEqual(children.count, (NSUInteger)2);
+    XCTAssertEqualObjects(children[0].inputItem.name, @"One");
+    XCTAssertEqualObjects(children[1].inputItem.name, @"Two");
+    XCTAssertEqualObjects(children[0].inputItem.MRL, @"file:///tmp/One.mp4");
+    XCTAssertEqualObjects(children[1].inputItem.MRL, @"file:///tmp/Two.mp4");
+    XCTAssertEqual(children[0].vlcInputItemNode, rootNode->pp_children[0]);
+    XCTAssertEqual(children[1].vlcInputItemNode, rootNode->pp_children[1]);
+
+    NSString * const description = inputNode.description;
+    XCTAssertTrue([description containsString:@"Root"]);
+    XCTAssertTrue([description containsString:@"number of children: 2"]);
+
+    input_item_node_Delete(rootNode);
+    input_item_Release(rootItem);
+    input_item_Release(childOne);
+    input_item_Release(childTwo);
+}
+
+- (void)testLeafNodeWithAnInputItemHasNoChildren
+{
+    input_item_t * const item =
+        input_item_NewFile("file:///tmp/Leaf.mp4", "Leaf", VLC_TICK_FROM_SEC(3), ITEM_LOCAL);
+    input_item_node_t * const node = input_item_node_Create(item);
+    VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:node];
+
+    XCTAssertEqual(inputNode.vlcInputItemNode, node);
+    XCTAssertEqualObjects(inputNode.inputItem.name, @"Leaf");
+    XCTAssertEqual(inputNode.numberOfChildren, 0);
+    XCTAssertNotNil(inputNode.children);
+    XCTAssertEqual(inputNode.children.count, (NSUInteger)0);
+    XCTAssertTrue([inputNode.description containsString:@"number of children: 0"]);
+
+    input_item_node_Delete(node);
+    input_item_Release(item);
+}
+
+- (void)testNestedNodesAreWrappedRecursively
+{
+    input_item_t * const rootItem =
+        input_item_NewDirectory("vlc://root", "Root", ITEM_LOCAL);
+    input_item_t * const directoryItem =
+        input_item_NewDirectory("vlc://directory", "Directory", ITEM_LOCAL);
+    input_item_t * const leafItem =
+        input_item_NewFile("file:///tmp/Nested.mp4", "Nested", VLC_TICK_FROM_SEC(4), ITEM_LOCAL);
+
+    input_item_node_t * const rootNode = input_item_node_Create(rootItem);
+    input_item_node_t * const directoryNode =
+        input_item_node_AppendItem(rootNode, directoryItem);
+    input_item_node_t * const leafNode =
+        input_item_node_AppendItem(directoryNode, leafItem);
+
+    VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:rootNode];
+    VLCInputNode * const directoryInputNode = inputNode.children.firstObject;
+    VLCInputNode * const leafInputNode = directoryInputNode.children.firstObject;
+
+    XCTAssertEqual(inputNode.numberOfChildren, 1);
+    XCTAssertEqual(directoryInputNode.vlcInputItemNode, directoryNode);
+    XCTAssertEqualObjects(directoryInputNode.inputItem.name, @"Directory");
+    XCTAssertEqual(directoryInputNode.numberOfChildren, 1);
+    XCTAssertEqual(leafInputNode.vlcInputItemNode, leafNode);
+    XCTAssertEqualObjects(leafInputNode.inputItem.name, @"Nested");
+    XCTAssertEqual(leafInputNode.numberOfChildren, 0);
+    XCTAssertEqualObjects(leafInputNode.children, @[]);
+
+    input_item_node_Delete(rootNode);
+    input_item_Release(rootItem);
+    input_item_Release(directoryItem);
+    input_item_Release(leafItem);
+}
+
+- (void)testChildrenAreCachedUntilCleared
+{
+    input_item_t *rootItem = NULL;
+    input_item_t *childOne = NULL;
+    input_item_t *childTwo = NULL;
+    input_item_node_t * const rootNode =
+        [self newRootNodeWithRootItem:&rootItem childOne:&childOne childTwo:&childTwo];
+    VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:rootNode];
+
+    NSArray<VLCInputNode *> * const initialChildren = inputNode.children;
+    XCTAssertEqual(initialChildren.count, (NSUInteger)1);
+    XCTAssertEqual(inputNode.numberOfChildren, 1);
+
+    input_item_node_AppendItem(rootNode, childTwo);
+    XCTAssertEqual(inputNode.numberOfChildren, 1);
+    XCTAssertEqual(inputNode.children, initialChildren);
+
+    [inputNode clearChildrenCache];
+    XCTAssertEqual(inputNode.numberOfChildren, 2);
+    NSArray<VLCInputNode *> * const refreshedChildren = inputNode.children;
+    XCTAssertNotEqual(refreshedChildren, initialChildren);
+    XCTAssertEqual(refreshedChildren.count, (NSUInteger)2);
+    XCTAssertEqualObjects(refreshedChildren[1].inputItem.name, @"Two");
+
+    input_item_node_Delete(rootNode);
+    input_item_Release(rootItem);
+    input_item_Release(childOne);
+    input_item_Release(childTwo);
+}
+
+- (void)testNodeWithoutAnInputItem
+{
+    input_item_node_t node = { 0 };
+    VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:&node];
+
+    XCTAssertNotNil(inputNode);
+    XCTAssertEqual(inputNode.vlcInputItemNode, &node);
+    XCTAssertNil(inputNode.inputItem);
+    XCTAssertEqual(inputNode.numberOfChildren, 0);
+    XCTAssertEqualObjects(inputNode.children, @[]);
+    XCTAssertTrue([inputNode.description containsString:@"p_item == nil"]);
+}
+
+- (void)testNilNodeHasEmptyState
+{
+    input_item_node_t * const nilNode = NULL;
+    VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:nilNode];
+
+    XCTAssertNotNil(inputNode);
+    XCTAssertTrue(inputNode.vlcInputItemNode == NULL);
+    XCTAssertNil(inputNode.inputItem);
+    XCTAssertEqual(inputNode.numberOfChildren, 0);
+    XCTAssertNil(inputNode.children);
+    [inputNode clearChildrenCache];
+    XCTAssertNil(inputNode.children);
+    XCTAssertTrue([inputNode.description containsString:@"p_item == nil"]);
+}
+
+- (void)testMalformedNodeWithMissingChildrenArrayDoesNotCrash
+{
+    input_item_node_t node = {
+        .p_item = NULL,
+        .i_children = 1,
+        .pp_children = NULL,
+    };
+    VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:&node];
+
+    XCTAssertEqual(inputNode.numberOfChildren, 1);
+    XCTAssertEqualObjects(inputNode.children, @[]);
+    XCTAssertEqual(inputNode.numberOfChildren, 0);
+}
+
+- (void)testMalformedNodeWithNullChildSlotIsWrappedSafely
+{
+    input_item_node_t *nullChild = NULL;
+    input_item_node_t node = {
+        .p_item = NULL,
+        .i_children = 1,
+        .pp_children = &nullChild,
+    };
+    VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:&node];
+
+    NSArray<VLCInputNode *> * const children = inputNode.children;
+    XCTAssertEqual(inputNode.numberOfChildren, 1);
+    XCTAssertEqual(children.count, (NSUInteger)1);
+    XCTAssertTrue(children.firstObject.vlcInputItemNode == NULL);
+    XCTAssertNil(children.firstObject.inputItem);
+    XCTAssertNil(children.firstObject.children);
+}
+
+ at end
+
+#pragma clang diagnostic pop


=====================================
modules/gui/macosx/tests/VLCLogMessageTest.m
=====================================
@@ -0,0 +1,124 @@
+/****************************************************************************
+ * VLCLogMessageTest.m: VLC log message native tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+
+#import "windows/logging/VLCLogMessage.h"
+#import <vlc_messages.h>
+
+ at interface VLCLogMessageTest : XCTestCase
+ at end
+
+ at implementation VLCLogMessageTest
+
+- (vlc_log_t)logInfo
+{
+    vlc_log_t info = { 0 };
+    info.psz_module = "core";
+    info.file = "input.c";
+    info.line = 42;
+    info.func = "OpenInput";
+    return info;
+}
+
+- (void)testMessageProperties
+{
+    vlc_log_t info = [self logInfo];
+    char message[] = "Could not open media";
+
+    VLCLogMessage * const logMessage =
+        [[VLCLogMessage alloc] initWithMessage:message
+                                           type:VLC_MSG_ERR
+                                           info:&info];
+
+    XCTAssertNotNil(logMessage);
+    XCTAssertEqual(logMessage.type, VLC_MSG_ERR);
+    XCTAssertEqualObjects(logMessage.message, @"Could not open media");
+    XCTAssertEqualObjects(logMessage.component, @"core");
+    XCTAssertEqualObjects(logMessage.function, @"OpenInput");
+    XCTAssertEqualObjects(logMessage.location, @"input.c:42");
+    XCTAssertEqualObjects(logMessage.typeName, @"error");
+    XCTAssertEqualObjects(logMessage.fullMessage, @"core error: Could not open media");
+}
+
+- (void)testFactoryMethod
+{
+    vlc_log_t info = [self logInfo];
+    char message[] = "Started";
+
+    VLCLogMessage * const logMessage = [VLCLogMessage logMessage:message
+                                                              type:VLC_MSG_INFO
+                                                              info:&info];
+
+    XCTAssertNotNil(logMessage);
+    XCTAssertEqualObjects(logMessage.fullMessage, @"core info: Started");
+}
+
+- (void)testTypeNames
+{
+    const struct {
+        int type;
+        NSString *name;
+    } cases[] = {
+        { VLC_MSG_INFO, @"info" },
+        { VLC_MSG_ERR, @"error" },
+        { VLC_MSG_WARN, @"warning" },
+        { VLC_MSG_DBG, @"debug" },
+        { 99, @"unknown" },
+    };
+
+    for (NSUInteger index = 0; index < sizeof(cases) / sizeof(cases[0]); index++) {
+        vlc_log_t info = [self logInfo];
+        char message[] = "Message";
+        VLCLogMessage * const logMessage =
+            [[VLCLogMessage alloc] initWithMessage:message
+                                               type:cases[index].type
+                                               info:&info];
+
+        XCTAssertEqualObjects(logMessage.typeName, cases[index].name);
+    }
+}
+
+- (void)testEmptyMessage
+{
+    vlc_log_t info = [self logInfo];
+    char message[] = "";
+
+    VLCLogMessage * const logMessage =
+        [[VLCLogMessage alloc] initWithMessage:message
+                                           type:VLC_MSG_INFO
+                                           info:&info];
+
+    XCTAssertNotNil(logMessage);
+    XCTAssertEqualObjects(logMessage.message, @"");
+    XCTAssertEqualObjects(logMessage.fullMessage, @"core info: ");
+}
+
+- (void)testRejectsMissingMessageOrLogInfo
+{
+    vlc_log_t info = [self logInfo];
+
+    XCTAssertNil([[VLCLogMessage alloc] initWithMessage:NULL
+                                                   type:VLC_MSG_INFO
+                                                   info:&info]);
+    XCTAssertNil([[VLCLogMessage alloc] initWithMessage:(char *)"Message"
+                                                   type:VLC_MSG_INFO
+                                                   info:NULL]);
+
+    info.psz_module = NULL;
+    XCTAssertNil([[VLCLogMessage alloc] initWithMessage:(char *)"Message"
+                                                   type:VLC_MSG_INFO
+                                                   info:&info]);
+}
+
+ at end


=====================================
modules/gui/macosx/tests/VLCTimeFormatterTest.m
=====================================
@@ -0,0 +1,88 @@
+/****************************************************************************
+ * VLCTimeFormatterTest.m: VLCTimeFormatter native tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+
+#import "views/VLCTimeFormatter.h"
+
+ at interface VLCTimeFormatterTest : XCTestCase
+ at end
+
+ at implementation VLCTimeFormatterTest
+
+- (void)testStringForObjectValue
+{
+    VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+
+    XCTAssertEqualObjects([formatter stringForObjectValue:@83], @"01:23");
+    XCTAssertEqualObjects([formatter stringForObjectValue:@3601], @"1:00:01");
+    XCTAssertEqualObjects([formatter stringForObjectValue:@0], @"--:--");
+}
+
+- (void)testGetObjectValueFromMinutesAndSeconds
+{
+    VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+    id object = nil;
+    NSString *error = nil;
+
+    XCTAssertTrue([formatter getObjectValue:&object
+                                  forString:@"01:23"
+                           errorDescription:&error]);
+    XCTAssertEqualObjects(object, @83000);
+    XCTAssertNil(error);
+}
+
+- (void)testGetObjectValueFromSeconds
+{
+    VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+    id object = nil;
+
+    XCTAssertTrue([formatter getObjectValue:&object
+                                  forString:@"83"
+                           errorDescription:nil]);
+    XCTAssertEqualObjects(object, @83000);
+}
+
+- (void)testGetObjectValueFromHoursMinutesAndSeconds
+{
+    VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+    id object = nil;
+
+    XCTAssertTrue([formatter getObjectValue:&object
+                                  forString:@"1:02:03"
+                           errorDescription:nil]);
+    XCTAssertEqualObjects(object, @3723000);
+}
+
+- (void)testGetObjectValueRejectsInvalidComponentCount
+{
+    VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+    id object = nil;
+    NSString *error = nil;
+
+    XCTAssertFalse([formatter getObjectValue:&object
+                                   forString:@"1:2:3:4"
+                            errorDescription:&error]);
+    XCTAssertNil(object);
+    XCTAssertNotNil(error);
+
+    object = nil;
+    error = nil;
+    XCTAssertFalse([formatter getObjectValue:&object
+                                   forString:@""
+                            errorDescription:&error]);
+    XCTAssertNil(object);
+    XCTAssertNotNil(error);
+}
+
+ at end


=====================================
modules/gui/macosx/views/VLCTimeFormatter.m
=====================================
@@ -39,22 +39,22 @@
 {
     NSArray<NSString *> * const components = [string componentsSeparatedByString:@":"];
     const NSUInteger componentCount = components.count;
-    if (componentCount <= 1 || componentCount > 3) {
+    if (string.length == 0 || componentCount > 3) {
         *error = @"Cannot get bookmark time as invalid string format for time was received";
         return NO;
     }
 
-    size_t time = 0;
+    int64_t time = 0;
 
     if (componentCount == 1) {
         time = components.firstObject.longLongValue * 1000;
     } else if (componentCount == 2) {
-        time = components.firstObject.longLongValue * 60 +
-               [components objectAtIndex:1].longLongValue * 1000;
+        time = (components.firstObject.longLongValue * 60 +
+                [components objectAtIndex:1].longLongValue) * 1000;
     } else if (componentCount == 3) {
-        time = components.firstObject.longLongValue * 3600 +
-               [components objectAtIndex:1].longLongValue * 60 +
-               [components objectAtIndex:2].longLongValue * 1000;
+        time = (components.firstObject.longLongValue * 3600 +
+                [components objectAtIndex:1].longLongValue * 60 +
+                [components objectAtIndex:2].longLongValue) * 1000;
     }
 
     *obj = [NSNumber numberWithLongLong:time];


=====================================
modules/gui/macosx/windows/logging/VLCLogMessage.m
=====================================
@@ -33,7 +33,7 @@
 {
     self = [super init];
     if (self) {
-        if (!message || !info->psz_module)
+        if (!message || !info || !info->psz_module)
             return nil;
         
         _type = type;



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/348464878bd258293b464098ecc997adef57e016...44c4002ead30fca927627d45a4a5fe6306f3e461

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/348464878bd258293b464098ecc997adef57e016...44c4002ead30fca927627d45a4a5fe6306f3e461
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list