diff --git a/.github/workflows/glt-ci.yaml b/.github/workflows/glt-ci.yaml
new file mode 100644
index 00000000..ceb50427
--- /dev/null
+++ b/.github/workflows/glt-ci.yaml
@@ -0,0 +1,97 @@
+name: GUI Layout Toolbox Continuous Integration
+
+# Controls when the workflow will run.
+on:
+
+ # Triggers the workflow on push or pull request events, but only for the master branch.
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+ # This allows the workflow run to be run manually from the Actions tab in GitHub.
+ workflow_dispatch:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel.
+jobs:
+
+ # Run the GUI Layout Toolbox tests.
+ run-GLT-tests:
+
+ # Define the job strategy.
+ strategy:
+
+ # Set up the job strategy matrix to define the different job configurations.
+ matrix:
+
+ # List of platforms on which to run the tests.
+ platform: [ ubuntu-latest, windows-latest ]
+
+ # List of MATLAB releases over which to run the tests.
+ matlab-version: [ R2020b, R2021a, R2021b, R2022a, R2022b, R2023a, R2023b, R2024a ]
+
+ # We don't define any startup options until we reach R2023b (handled separately below).
+ matlab-startup-options: [ '' ]
+
+ # Windows/Mac are supported from R2021a onwards. Ubuntu is supported from R2020b onwards.
+ # Exclude the Windows job on R2020b.
+ exclude:
+
+ - platform: windows-latest
+ matlab-version: R2020b
+ matlab-startup-options: [ '' ]
+
+ # The tests should also be run in the JavaScript Desktop from R2023b onwards (this is the -webui startup option).
+ include:
+
+ - platform: ubuntu-latest
+ matlab-version: R2023b
+ matlab-startup-options: -webui
+
+ - platform: windows-latest
+ matlab-version: R2023b
+ matlab-startup-options: -webui
+
+ - platform: ubuntu-latest
+ matlab-version: R2024a
+ matlab-startup-options: -webui
+
+ - platform: windows-latest
+ matlab-version: R2024a
+ matlab-startup-options: -webui
+
+ # Specify the platform that the job will run on.
+ runs-on: ${{ matrix.platform }}
+
+ # Don't fail the entire run if one job fails.
+ continue-on-error: true
+
+ # Steps define a sequence of tasks to be executed as part of the job.
+ steps:
+
+ # Check out the repository under $GITHUB_WORKSPACE, so that the job can access it.
+ - name: Check out the repository
+ uses: actions/checkout@v4
+
+ # For Linux jobs, start a display server on the runner.
+ - name: Start a display server for jobs running on Linux.
+ if: ${{ matrix.platform == 'ubuntu-latest' }}
+ run: |
+ sudo apt-get install -y xvfb
+ Xvfb :99 &
+ echo "DISPLAY=:99" >> $GITHUB_ENV
+
+ # Set up MATLAB on the runner.
+ - name: Set up MATLAB on the runner.
+ uses: matlab-actions/setup-matlab@v2
+ with:
+ # The tests require only base MATLAB.
+ products: MATLAB
+ release: ${{ matrix.matlab-version }}
+
+ # Run the GUI Layout Toolbox tests.
+ - name: Run the GUI Layout Toolbox tests.
+ uses: matlab-actions/run-command@v2
+ with:
+ startup-options: ${{ matrix.matlab-startup-options }}
+ command: openProject("project.prj"); results = runToolboxTests(); failedTests = table(results([results.Failed])); disp(failedTests); results.assertSuccess();
\ No newline at end of file
diff --git a/docsrc/Examples/TEST_REQUIREMENTS.xml b/docsrc/Examples/TEST_REQUIREMENTS.xml
deleted file mode 100644
index d5b27830..00000000
--- a/docsrc/Examples/TEST_REQUIREMENTS.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docsrc/Examples/callbackexample.m b/docsrc/Examples/callbackexample.m
index 3262dc50..1721948d 100644
--- a/docsrc/Examples/callbackexample.m
+++ b/docsrc/Examples/callbackexample.m
@@ -1,4 +1,4 @@
-function callbackexample()
+function varargout = callbackexample()
% Copyright 2009-2020 The MathWorks, Inc.
@@ -37,11 +37,15 @@ function callbackexample()
% Add user interactions
set( hList, 'Callback', @onChangeColor );
+% Return output.
+if nargout > 0
+ nargoutchk( 1, 1 )
+ varargout{1} = f;
+end % if
function onChangeColor( source, ~ )
idx = get( source, 'Value' );
set( hButton, 'Background', colorValues(idx,:), 'String', colorNames{idx} )
end % onChangeColor
-
end % main
\ No newline at end of file
diff --git a/docsrc/Examples/demoBrowser.m b/docsrc/Examples/demoBrowser.m
index 6f932ea4..91c52ba7 100644
--- a/docsrc/Examples/demoBrowser.m
+++ b/docsrc/Examples/demoBrowser.m
@@ -1,4 +1,4 @@
-function demoBrowser()
+function varargout = demoBrowser()
%demoBrowser: an example of using layouts to build a user interface
%
% demoBrowser() opens a simple GUI that allows several of MATLAB's
@@ -25,6 +25,12 @@ function demoBrowser()
% Explicitly call the demo display so that it gets included if we deploy
displayEndOfDemoMessage('')
+% Return output.
+if nargout > 0
+ nargoutchk( 1, 1 )
+ varargout{1} = gui.Window;
+end % if
+
%-------------------------------------------------------------------------%
function data = createData()
% Create the shared data-structure for this application
diff --git a/docsrc/Examples/dockexample.m b/docsrc/Examples/dockexample.m
index d1a049e6..f7c77ebc 100644
--- a/docsrc/Examples/dockexample.m
+++ b/docsrc/Examples/dockexample.m
@@ -1,4 +1,4 @@
-function dockexample()
+function varargout = dockexample()
%DOCKEXAMPLE: An example of using the panelbox dock/undock functionality
% Copyright 2009-2020 The MathWorks, Inc.
@@ -30,6 +30,12 @@ function dockexample()
set( panel{2}, 'DockFcn', {@nDock, 2} );
set( panel{3}, 'DockFcn', {@nDock, 3} );
+% Return output.
+if nargout > 0
+ nargoutchk( 1, 1 )
+ varargout{1} = fig;
+end % if
+
%-------------------------------------------------------------------------%
function nDock( eventSource, eventData, whichpanel ) %#ok
% Set the flag
diff --git a/docsrc/Examples/minimizeexample.m b/docsrc/Examples/minimizeexample.m
index 741cd16d..5d98ea4c 100644
--- a/docsrc/Examples/minimizeexample.m
+++ b/docsrc/Examples/minimizeexample.m
@@ -1,4 +1,4 @@
-function minimizeexample()
+function varargout = minimizeexample()
%MINIMIZEEXAMPLE: An example of using the panelbox minimize/maximize
% Copyright 2009-2020 The MathWorks, Inc.
@@ -33,6 +33,12 @@ function minimizeexample()
set( panel{2}, 'MinimizeFcn', {@nMinimize, 2} );
set( panel{3}, 'MinimizeFcn', {@nMinimize, 3} );
+% Return output.
+if nargout > 0
+ nargoutchk( 1, 1 )
+ varargout{1} = fig;
+end % if
+
%-------------------------------------------------------------------------%
function nMinimize( eventSource, eventData, whichpanel ) %#ok
% A panel has been maximized/minimized
@@ -45,7 +51,7 @@ function nMinimize( eventSource, eventData, whichpanel ) %#ok
s(whichpanel) = pheightmax;
end
set( box, 'Heights', s );
-
+
% Resize the figure, keeping the top stationary
delta_height = pos(1,4) - sum( box.Heights );
set( fig, 'Position', pos(1,:) + [0 delta_height 0 -delta_height] );
diff --git a/resources/project/Root.type.Files/.github.type.File.xml b/resources/project/Root.type.Files/.github.type.File.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/Root.type.Files/.github.type.File.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/Root.type.Files/.github.type.File/1.type.DIR_SIGNIFIER.xml b/resources/project/Root.type.Files/.github.type.File/1.type.DIR_SIGNIFIER.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/Root.type.Files/.github.type.File/1.type.DIR_SIGNIFIER.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/Root.type.Files/.github.type.File/workflows.type.File.xml b/resources/project/Root.type.Files/.github.type.File/workflows.type.File.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/Root.type.Files/.github.type.File/workflows.type.File.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/Root.type.Files/.github.type.File/workflows.type.File/1.type.DIR_SIGNIFIER.xml b/resources/project/Root.type.Files/.github.type.File/workflows.type.File/1.type.DIR_SIGNIFIER.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/Root.type.Files/.github.type.File/workflows.type.File/1.type.DIR_SIGNIFIER.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/Root.type.Files/.github.type.File/workflows.type.File/glt-ci.yaml.type.File.xml b/resources/project/Root.type.Files/.github.type.File/workflows.type.File/glt-ci.yaml.type.File.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/Root.type.Files/.github.type.File/workflows.type.File/glt-ci.yaml.type.File.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/Root.type.Files/docsrc.type.File/Examples.type.File/TEST_REQUIREMENTS.xml.type.File.xml b/resources/project/Root.type.Files/docsrc.type.File/Examples.type.File/TEST_REQUIREMENTS.xml.type.File.xml
deleted file mode 100644
index 75e6825d..00000000
--- a/resources/project/Root.type.Files/docsrc.type.File/Examples.type.File/TEST_REQUIREMENTS.xml.type.File.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/resources/project/Root.type.Files/releases.type.File/GUI Layout Toolbox 2.3.6.mltbx.type.File.xml b/resources/project/Root.type.Files/releases.type.File/GUI Layout Toolbox 2.3.6.mltbx.type.File.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/Root.type.Files/releases.type.File/GUI Layout Toolbox 2.3.6.mltbx.type.File.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/resources/project/Root.type.Files/tbx.type.File/layoutdoc.type.File/Examples.type.File/TEST_REQUIREMENTS.xml.type.File.xml b/resources/project/Root.type.Files/tbx.type.File/layoutdoc.type.File/Examples.type.File/TEST_REQUIREMENTS.xml.type.File.xml
deleted file mode 100644
index 75e6825d..00000000
--- a/resources/project/Root.type.Files/tbx.type.File/layoutdoc.type.File/Examples.type.File/TEST_REQUIREMENTS.xml.type.File.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/resources/project/Root.type.Files/tbx.type.File/layoutdoc.type.File/custom_toolbox.json.type.File.xml b/resources/project/Root.type.Files/tbx.type.File/layoutdoc.type.File/custom_toolbox.json.type.File.xml
new file mode 100644
index 00000000..a75f7a81
--- /dev/null
+++ b/resources/project/Root.type.Files/tbx.type.File/layoutdoc.type.File/custom_toolbox.json.type.File.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/tbx/layout/+uix/TabPanel.m b/tbx/layout/+uix/TabPanel.m
index c7b12a36..aec19b89 100644
--- a/tbx/layout/+uix/TabPanel.m
+++ b/tbx/layout/+uix/TabPanel.m
@@ -20,7 +20,7 @@
FontName % font name
FontSize % font size
FontWeight % font weight
- FontUnits % font weight
+ FontUnits % font units
ForegroundColor % tab text color [RGB]
HighlightColor % border highlight color [RGB]
ShadowColor % border shadow color [RGB]
@@ -120,6 +120,10 @@
end % get.FontAngle
function set.FontAngle( obj, value )
+
+ if ~verLessThan( 'matlab', '9.3' )
+ value = convertStringsToChars( value );
+ end % if
% Check
assert( ischar( value ) && any( strcmp( value, {'normal','italic','oblique'} ) ), ...
@@ -150,6 +154,10 @@
end % get.FontName
function set.FontName( obj, value )
+
+ if ~verLessThan( 'matlab', '9.3' )
+ value = convertStringsToChars( value );
+ end % if
% Check
assert( ischar( value ) && any( strcmp( value, obj.FontNames ) ), ...
@@ -212,6 +220,10 @@
end % get.FontWeight
function set.FontWeight( obj, value )
+
+ if ~verLessThan( 'matlab', '9.3' )
+ value = convertStringsToChars( value );
+ end % if
% Check
assert( ischar( value ) && any( strcmp( value, {'normal','bold'} ) ), ...
@@ -242,6 +254,10 @@
end % get.FontUnits
function set.FontUnits( obj, value )
+
+ if ~verLessThan( 'matlab', '9.3' )
+ value = convertStringsToChars( value );
+ end % if
% Check
assert( ischar( value ) && ...
@@ -400,6 +416,10 @@
end % get.TabEnables
function set.TabEnables( obj, value )
+
+ if ~verLessThan( 'matlab', '9.3' )
+ value = cellstr( convertStringsToChars( value ) );
+ end % if
% For those who can't tell a column from a row...
if isrow( value )
@@ -415,7 +435,7 @@
isequal( size( value ), size( tabs ) ) && ...
all( strcmp( value, 'on' ) | strcmp( value, 'off' ) ), ...
'uix:InvalidPropertyValue', ...
- 'Property ''TabEnables'' should be a cell array of strings ''on'' or ''off'', one per tab.' )
+ 'Property ''TabEnables'' should be a cell array of character vectors ''on'' or ''off'', or an array of strings, one per tab.' ) %#ok
% Set
tf = strcmp( value, 'on' );
@@ -440,6 +460,10 @@
end % get.TabLocation
function set.TabLocation( obj, value )
+
+ if ~verLessThan( 'matlab', '9.3' )
+ value = convertStringsToChars( value );
+ end % if
% Check
assert( ischar( value ) && ...
@@ -462,6 +486,10 @@
end % get.TabTitles
function set.TabTitles( obj, value )
+
+ if ~verLessThan( 'matlab', '9.3' )
+ value = cellstr( convertStringsToChars( value ) );
+ end % if
% For those who can't tell a column from a row...
if isrow( value )
@@ -475,7 +503,7 @@
assert( iscellstr( value ) && ...
isequal( size( value ), size( tabs ) ), ...
'uix:InvalidPropertyValue', ...
- 'Property ''TabTitles'' should be a cell array of strings, one per tab.' )
+ 'Property ''TabTitles'' should be a cell array of character vectors or an array of strings, one per tab.' ) %#ok
% Set
n = numel( tabs );
@@ -863,9 +891,14 @@ function onParentChanged( obj, ~, ~ )
end
if ~isempty( prop )
- obj.ParentBackgroundColorListener = event.proplistener( obj.Parent, ...
- findprop( obj.Parent, prop ), 'PostSet', ...
- @( src, evt ) obj.updateParentBackgroundColor( prop ) );
+ foundProp = findprop( obj.Parent, prop );
+ if foundProp.SetObservable
+ obj.ParentBackgroundColorListener = event.proplistener( obj.Parent, ...
+ foundProp, 'PostSet', ...
+ @( src, evt ) obj.updateParentBackgroundColor( prop ) );
+ else
+ obj.ParentBackgroundColorListener = [];
+ end % if
else
obj.ParentBackgroundColorListener = [];
end
diff --git a/tbx/layoutdoc/Examples/TEST_REQUIREMENTS.xml b/tbx/layoutdoc/Examples/TEST_REQUIREMENTS.xml
deleted file mode 100644
index d5b27830..00000000
--- a/tbx/layoutdoc/Examples/TEST_REQUIREMENTS.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tbx/layoutdoc/Examples/callbackexample.m b/tbx/layoutdoc/Examples/callbackexample.m
index 3262dc50..1721948d 100644
--- a/tbx/layoutdoc/Examples/callbackexample.m
+++ b/tbx/layoutdoc/Examples/callbackexample.m
@@ -1,4 +1,4 @@
-function callbackexample()
+function varargout = callbackexample()
% Copyright 2009-2020 The MathWorks, Inc.
@@ -37,11 +37,15 @@ function callbackexample()
% Add user interactions
set( hList, 'Callback', @onChangeColor );
+% Return output.
+if nargout > 0
+ nargoutchk( 1, 1 )
+ varargout{1} = f;
+end % if
function onChangeColor( source, ~ )
idx = get( source, 'Value' );
set( hButton, 'Background', colorValues(idx,:), 'String', colorNames{idx} )
end % onChangeColor
-
end % main
\ No newline at end of file
diff --git a/tbx/layoutdoc/Examples/demoBrowser.m b/tbx/layoutdoc/Examples/demoBrowser.m
index 6f932ea4..91c52ba7 100644
--- a/tbx/layoutdoc/Examples/demoBrowser.m
+++ b/tbx/layoutdoc/Examples/demoBrowser.m
@@ -1,4 +1,4 @@
-function demoBrowser()
+function varargout = demoBrowser()
%demoBrowser: an example of using layouts to build a user interface
%
% demoBrowser() opens a simple GUI that allows several of MATLAB's
@@ -25,6 +25,12 @@ function demoBrowser()
% Explicitly call the demo display so that it gets included if we deploy
displayEndOfDemoMessage('')
+% Return output.
+if nargout > 0
+ nargoutchk( 1, 1 )
+ varargout{1} = gui.Window;
+end % if
+
%-------------------------------------------------------------------------%
function data = createData()
% Create the shared data-structure for this application
diff --git a/tbx/layoutdoc/Examples/dockexample.m b/tbx/layoutdoc/Examples/dockexample.m
index d1a049e6..f7c77ebc 100644
--- a/tbx/layoutdoc/Examples/dockexample.m
+++ b/tbx/layoutdoc/Examples/dockexample.m
@@ -1,4 +1,4 @@
-function dockexample()
+function varargout = dockexample()
%DOCKEXAMPLE: An example of using the panelbox dock/undock functionality
% Copyright 2009-2020 The MathWorks, Inc.
@@ -30,6 +30,12 @@ function dockexample()
set( panel{2}, 'DockFcn', {@nDock, 2} );
set( panel{3}, 'DockFcn', {@nDock, 3} );
+% Return output.
+if nargout > 0
+ nargoutchk( 1, 1 )
+ varargout{1} = fig;
+end % if
+
%-------------------------------------------------------------------------%
function nDock( eventSource, eventData, whichpanel ) %#ok
% Set the flag
diff --git a/tbx/layoutdoc/Examples/minimizeexample.m b/tbx/layoutdoc/Examples/minimizeexample.m
index 741cd16d..d8781aaf 100644
--- a/tbx/layoutdoc/Examples/minimizeexample.m
+++ b/tbx/layoutdoc/Examples/minimizeexample.m
@@ -1,4 +1,4 @@
-function minimizeexample()
+function varargout = minimizeexample()
%MINIMIZEEXAMPLE: An example of using the panelbox minimize/maximize
% Copyright 2009-2020 The MathWorks, Inc.
@@ -33,6 +33,12 @@ function minimizeexample()
set( panel{2}, 'MinimizeFcn', {@nMinimize, 2} );
set( panel{3}, 'MinimizeFcn', {@nMinimize, 3} );
+% Return output.
+if nargout > 0
+ nargoutchk( 1, 1 )
+ varargout{1} = fig;
+end % if
+
%-------------------------------------------------------------------------%
function nMinimize( eventSource, eventData, whichpanel ) %#ok
% A panel has been maximized/minimized
diff --git a/tests/+gesturetests/tTabPanelGestures.m b/tests/+gesturetests/tTabPanelGestures.m
index 0df53241..072744ec 100644
--- a/tests/+gesturetests/tTabPanelGestures.m
+++ b/tests/+gesturetests/tTabPanelGestures.m
@@ -11,17 +11,27 @@
function tClickingTabPassesEventData( testCase, ConstructorName )
- % Assume that we are in the web graphics case.
+ % Assume that we are working in web graphics in at least
+ % R2018a.
testCase.assumeGraphicsAreWebBased()
+ testCase.assumeMATLABVersionIsAtLeast( 'R2018a' )
- % Create a tab panel.
+ % Using the App Testing Framework with GitHub Actions is
+ % supported from R2023b onwards.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2023b' )
+ end % if
+
+ % Create a tab panel in a grid layout.
testFig = testCase.ParentFixture.Parent;
- tabPanel = feval( ConstructorName, 'Parent', testFig );
+ testGrid = uigridlayout( testFig, [1, 1], 'Padding', 0 );
+ tabPanel = feval( ConstructorName, 'Parent', testGrid );
% Add two controls.
uicontrol( 'Parent', tabPanel )
uicontrol( 'Parent', tabPanel )
-
+
% Create a listener.
eventRaised = false;
eventData = struct();
@@ -40,7 +50,7 @@ function tClickingTabPassesEventData( testCase, ConstructorName )
% Use the app testing framework to click the second tab to
% change the selection.
testCase.press( testFig, [2*tabWidth-5, figureHeight-10] )
-
+
% Verify that the event was raised.
testCase.verifyTrue( eventRaised, ...
['Clicking on another tab to change the selection ', ...
diff --git a/tests/+glttestutilities/TestInfrastructure.m b/tests/+glttestutilities/TestInfrastructure.m
index c86b160f..cf542439 100644
--- a/tests/+glttestutilities/TestInfrastructure.m
+++ b/tests/+glttestutilities/TestInfrastructure.m
@@ -129,8 +129,32 @@ function assumeMATLABVersionIsAtLeast( testCase, versionString )
versionNumber = '9.1';
case 'R2017a'
versionNumber = '9.2';
+ case 'R2017b'
+ versionNumber = '9.3';
+ case 'R2018a'
+ versionNumber = '9.4';
+ case 'R2018b'
+ versionNumber = '9.5';
+ case 'R2019a'
+ versionNumber = '9.6';
+ case 'R2019b'
+ versionNumber = '9.7';
+ case 'R2020a'
+ versionNumber = '9.8';
+ case 'R2020b'
+ versionNumber = '9.9';
+ case 'R2021a'
+ versionNumber = '9.10';
+ case 'R2021b'
+ versionNumber = '9.11';
case 'R2022a'
versionNumber = '9.12';
+ case 'R2022b'
+ versionNumber = '9.13';
+ case 'R2023a'
+ versionNumber = '9.14';
+ case 'R2023b'
+ versionNumber = '23.2';
otherwise
error( ['AssumeMATLABVersionIsAtLeast:', ...
'InvalidVersionString'], ...
@@ -193,27 +217,15 @@ function assumeGraphicsAreNotWebBased( testCase )
end % assumeGraphicsAreNotWebBased
- function assumeTestEnvironmentHasDisplay( testCase )
+ function assumeJavaScriptDesktop( testCase )
- % Check that the test environment has a display. This is
- % required for the mouse tests used for the flexible
- % containers.
- currentFolder = fileparts( mfilename( 'fullpath' ) );
- BaTFolder = fullfile( matlabroot(), 'test', ...
- 'fileexchangeapps', 'GUI_layout_toolbox', 'tests' );
- inBaTFolder = strcmp( currentFolder, BaTFolder );
- testCase.assumeFalse( inBaTFolder, ...
- ['This test is not applicable in the BaT ', ...
- 'environment. A display is required to run ', ...
- 'the mouse tests.'] )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2022a' )
+ isJSD = feature( 'webui' );
+ testCase.assumeTrue( isJSD, ...
+ ['This test is only applicable in the new desktop ', ...
+ 'environment for MATLAB (the JavaScript Desktop).'] )
- % Check that the test environment is not Jenkins.
- isJenkins = ~isempty( getenv( 'JENKINS_HOME' ) );
- testCase.assumeFalse( isJenkins, ...
- ['This test is not applicable when running in ', ...
- 'the Jenkins environment.'] )
-
- end % assumeTestEnvironmentHasDisplay
+ end % assumeJavaScriptDesktop
function assumeNotMac( testCase )
@@ -229,7 +241,7 @@ function assumeNotUnix( testCase )
testCase.assumeFalse( isunix(), ...
'This test is not applicable on the Unix platform.' )
- end % assumeNotUnix
+ end % assumeNotUnix
function assumeNotDeployed( testCase )
diff --git a/tests/+sharedtests/SharedContainerTests.m b/tests/+sharedtests/SharedContainerTests.m
index a8d63790..e3b30fb5 100644
--- a/tests/+sharedtests/SharedContainerTests.m
+++ b/tests/+sharedtests/SharedContainerTests.m
@@ -359,13 +359,29 @@ function tAxesInComponentRemainsVisibleAfter3DRotation( ...
end % tAxesInComponentRemainsVisibleAfter3DRotation
- function tEnablingDataCursorModePreservesAxesPosition( ...
+ function tEnablingDataCursorModeIsWarningFree( ...
testCase, ConstructorName )
- % Data cursor mode only works in Java figures, so we need to
- % exclude the unrooted and Web figure cases.
- testCase.assumeGraphicsAreRooted()
- testCase.assumeGraphicsAreNotWebBased()
+ % Skip this test if we're running in CI.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ isci = ~isempty( ci ) && strcmp( ci, 'true' );
+ testCase.assumeFalse( isci, ...
+ ['This test is not applicable when running in ', ...
+ 'GitHub Actions.'] )
+
+ % Exclude the unrooted case.
+ testCase.assumeGraphicsAreRooted()
+
+ % Work around a bug in R2022a-R2023a by disabling a warning for
+ % the duration of the test.
+ v = ver( 'matlab' ); %#ok
+ v = v.Version;
+ if ismember( v, {'9.12', '9.13', '9.14'} )
+ warningID = 'MATLAB:callback:DynamicPropertyEventError';
+ warningState = warning( 'query', warningID );
+ warning( 'off', warningID )
+ warningCleanup = onCleanup( @() warning( warningState ) );
+ end % if
% Create the component.
component = testCase.constructComponent( ConstructorName );
@@ -381,25 +397,38 @@ function tEnablingDataCursorModePreservesAxesPosition( ...
% Plot into the axes.
p = plot( ax, 1:10 );
- % Enable data cursor mode.
- dcm = datacursormode( component.Parent );
- dcm.Enable = 'on';
- drawnow()
+ % Initialize a datacursor mode object.
+ dcm = [];
+
+ function enableDataCursorMode()
+
+ dcm = datacursormode( component.Parent );
+ dcm.Enable = 'on';
+ drawnow()
+
+ end % enableDataCursorMode
+
+ % Verify that there are no warnings when enabling datacursor
+ % mode.
+ enabler = @() enableDataCursorMode();
+ testCase.verifyWarningFree( enabler, ['Enabling data ', ...
+ 'cursor mode in a figure containing a ', ...
+ ConstructorName, ' component was not warning-free.'] )
+
+ function addDataTip()
- % Capture the current axes position, add a datatip, then
- % capture the axes position again.
- oldPosition = ax.Position;
- dcm.createDatatip( p );
- drawnow()
- newPosition = ax.Position;
+ dcm.createDatatip( p );
+ drawnow()
- % Verify that the axes 'Position' property has not changed.
- testCase.verifyEqual( newPosition, oldPosition, ...
- ['Enabling data cursor mode on an axes in a ', ...
- ConstructorName, ' component caused the axes ', ...
- '''Position'' property to change.'] )
+ end % addDataTip
- end % tEnablingDataCursorModePreservesAxesPosition
+ % Add a datatip and verify that no warnings occur.
+ dataTipAdder = @() addDataTip();
+ testCase.verifyWarningFree( dataTipAdder, ...
+ ['Adding a data tip to a plot inside a ', ...
+ ConstructorName, ' component was not warning-free.'] )
+
+ end % tEnablingDataCursorModeIsWarningFree
function tContentsRespectAddingAxesAndControl( ...
testCase, ConstructorName )
@@ -540,8 +569,10 @@ function tContainerDynamicEnableSetMethod( ...
end % for
% Check that setting an invalid value causes an error.
- if verLessThan( 'matlab', '9.9' )
+ if verLessThan( 'matlab', '9.9' ) %#ok<*VERLESSMATLAB>
errorID = 'uiextras:InvalidPropertyValue';
+ elseif verLessThan( 'matlab', '9.13' )
+ errorID = 'MATLAB:datatypes:InvalidEnumValueFor';
else
errorID = ...
'MATLAB:datatypes:onoffboolean:IncorrectValue';
@@ -751,19 +782,28 @@ function tGetAndSetMethodsFunctionCorrectly( ...
% Extract the current name-value pair.
propertyName = NameValuePairs{k};
propertyValue = NameValuePairs{k+1};
- % Set the property in the component.
- component.(propertyName) = propertyValue;
- % Verify that the property has been assigned correctly, up
- % to a possible data type conversion.
- actual = component.(propertyName);
- if ~isa( propertyValue, 'function_handle' )
- propertyClass = class( propertyValue );
- actual = feval( propertyClass, actual );
- end % if
- testCase.verifyEqual( actual, propertyValue, ...
- ['Setting the ''', propertyName, ''' property of ', ...
- 'the ', ConstructorName, ' object did not store ', ...
- 'the value correctly.'] )
+ try
+ % Set the property in the component.
+ component.(propertyName) = propertyValue;
+ % Verify that the property has been assigned correctly,
+ % up to a possible data type conversion.
+ actual = component.(propertyName);
+ if ~isa( propertyValue, 'function_handle' )
+ propertyClass = class( propertyValue );
+ actual = feval( propertyClass, actual );
+ end % if
+ testCase.verifyEqual( actual, propertyValue, ...
+ ['Setting the ''', propertyName, ...
+ ''' property of the ', ConstructorName, ...
+ ' object did not store the value correctly.'] )
+ catch e
+ newExc = MException( ['SharedContainerTests:', ...
+ 'SettingPropertyCausedError'], ...
+ ['Setting the property ', propertyName, ...
+ ' caused an error.'] );
+ newExc = newExc.addCause( e );
+ newExc.throw()
+ end % try/catch
end % for
end % tGetAndSetMethodsFunctionCorrectly
@@ -867,6 +907,17 @@ function assumeComponentIsAContainer( testCase, ConstructorName )
end % assumeComponentIsAContainer
+ function assumeNotButtonBox( testCase, ConstructorName )
+
+ % Assume that the component, specified by ConstructorName, is
+ % not a button box.
+ isabuttonbox = ismember( 'uix.ButtonBox', ...
+ superclasses( ConstructorName ) );
+ testCase.assumeFalse( isabuttonbox, ...
+ 'This test is not applicable to button boxes.' )
+
+ end % assumeNotButtonBox
+
end % methods ( Sealed, Access = protected )
end % class
diff --git a/tests/+sharedtests/SharedFlexTests.m b/tests/+sharedtests/SharedFlexTests.m
index 6a624518..346ed183 100644
--- a/tests/+sharedtests/SharedFlexTests.m
+++ b/tests/+sharedtests/SharedFlexTests.m
@@ -3,18 +3,25 @@
%(*.HBoxFlex, *.VBoxFlex, and *.GridFlex).
properties ( TestParameter )
- % Sample flexible layout children sizes. We need all pairwise
+ % Sample flexible layout children sizes. We need all pairwise
% combinations of relative and fixed sizes.
ChildrenSizes = {[-1, -1], [200, -1], [-1, 200], [200, 200]}
end % properties ( TestParameter )
- methods ( Test, Sealed )
+ methods ( Test, Sealed, TestTags = {'MovesMouse'} )
function tDraggingDividerIsWarningFree( ...
testCase, ConstructorName, ChildrenSizes )
- % Assume that the graphics are rooted.
- testCase.assumeGraphicsAreRooted()
+ % Assume that the graphics are rooted and in the JavaScript
+ % desktop.
+ testCase.assumeGraphicsAreRooted()
+
+ % If running in CI, assume we have at least R2023b.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci ) && strcmp( ci, 'true' )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2023b' )
+ end % if
% Create a component.
component = testCase.constructComponent( ConstructorName, ...
@@ -31,7 +38,7 @@ function tDraggingDividerIsWarningFree( ...
% Wait until the figure renders.
testFig = ancestor( component, 'figure' );
% Ensure the figure is not docked.
- testFig.WindowStyle = 'normal';
+ testFig.WindowStyle = 'normal';
isuifigure = isempty( get( testFig, 'JavaFrame_I' ) );
if isuifigure
pause( 5 )
@@ -101,9 +108,16 @@ function dragger( offset )
function tClickingFlexibleLayoutIsWarningFree( ...
testCase, ConstructorName )
- % Assume that the graphics are rooted.
+ % Assume that the graphics are rooted and in the JavaScript
+ % desktop.
testCase.assumeGraphicsAreRooted()
+ % If running in CI, assume we have at least R2023b.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci ) && strcmp( ci, 'true' )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2023b' )
+ end % if
+
% Create the component.
component = testCase.constructComponent( ConstructorName );
@@ -138,10 +152,19 @@ function clicker()
function tMouseOverDividerInDockedFigureUpdatesPointer( ...
testCase, ConstructorName )
- % This test only applies to figures that can be docked.
- testCase.assumeGraphicsAreRooted()
+ % Exclude unrooted and web graphics.
+ testCase.assumeGraphicsAreRooted()
testCase.assumeGraphicsAreNotWebBased()
+ % Exclude Mac OS.
+ testCase.assumeNotMac()
+
+ % If running in CI, assume we have at least R2023b.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci ) && strcmp( ci, 'true' )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2023b' )
+ end % if
+
% Create the flexible container.
component = testCase.constructComponent( ConstructorName, ...
'Padding', 10, ...
@@ -152,7 +175,7 @@ function tMouseOverDividerInDockedFigureUpdatesPointer( ...
uicontrol( 'Parent', component );
end % for
- % Dock the test figure, focus it, and
+ % Dock the test figure and focus it.
testFig = ancestor( component, 'figure' );
testFig.WindowStyle = 'docked';
windowStyleCleanup = onCleanup( ...
@@ -196,11 +219,57 @@ function tMouseOverDividerInDockedFigureUpdatesPointer( ...
end % tMouseOverDividerInDockedFigureUpdatesPointer
+ function tClickingDividerIsWarningFree( testCase, ConstructorName )
+
+ % This test is only for rooted components.
+ testCase.assumeGraphicsAreRooted()
+
+ % If running in CI, assume we have at least R2023b.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci ) && strcmp( ci, 'true' )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2023b' )
+ end % if
+
+ % Create the layout and add children.
+ [component, dividers] = createFlexibleLayoutWithChildren( ...
+ testCase, ConstructorName );
+
+ % Move the mouse to the center of a divider.
+ testFig = ancestor( component, 'figure' );
+ figureOrigin = getFigureOrigin( testFig );
+ dividerCenter = figureOrigin + ...
+ getpixelcenter( dividers(1), true );
+ moveMouseTo( dividerCenter )
+
+ % Verify that clicking the divider is warning-free.
+ testCase.verifyWarningFree( @clicker, ...
+ ['Clicking the divider in a ', ConstructorName, ...
+ ' component was not warning-free.'] )
+
+ function clicker()
+
+ % Create the robot.
+ bot = java.awt.Robot();
+
+ % Click.
+ bot.mousePress( java.awt.event.InputEvent.BUTTON1_MASK );
+ pause( 0.5 )
+
+ % Let go.
+ bot.mouseRelease( java.awt.event.InputEvent.BUTTON1_MASK );
+ pause( 0.5 )
+
+ end % clicker
+
+ end % tClickingDividerIsWarningFree
+
function tMousePointerUpdatesOnFlexChange( ...
testCase, ConstructorName )
- % This test is only for rooted components.
+ % This test is only for rooted components in the JavaScript
+ % desktop environment.
testCase.assumeGraphicsAreRooted()
+ testCase.assumeJavaScriptDesktop()
% Create the component
testFig = testCase.ParentFixture.Parent;
@@ -264,6 +333,7 @@ function tMousePointerUpdatesOnFlexChange( ...
buttonCenter = figureOrigin + ...
getpixelcenter( buttons1(1), true );
moveMouseTo( buttonCenter )
+ pause( 1 )
testCase.verifyEqual( testFig.Pointer, 'arrow', ...
['The mouse pointer did not change to ''arrow''', ...
' when moved over a button in a ', ConstructorName, ...
@@ -273,6 +343,7 @@ function tMousePointerUpdatesOnFlexChange( ...
dividerCenter = figureOrigin + ...
getpixelcenter( div1(end), true );
moveMouseTo( dividerCenter )
+ pause( 1 )
testCase.verifyMatches( testFig.Pointer, ...
'(left|right|top|bottom)', ...
['The mouse pointer did not change to ''left'', ', ...
@@ -284,6 +355,7 @@ function tMousePointerUpdatesOnFlexChange( ...
dividerCenter = figureOrigin + ...
getpixelcenter( div2(end), true );
moveMouseTo( dividerCenter )
+ pause( 1 )
testCase.verifyMatches( testFig.Pointer, ...
'(left|right|top|bottom)', ...
['The mouse pointer did not change to ''left'', ', ...
@@ -294,6 +366,7 @@ function tMousePointerUpdatesOnFlexChange( ...
buttonCenter = figureOrigin + ...
getpixelcenter( buttons2(2), true );
moveMouseTo( buttonCenter )
+ pause( 1 )
testCase.verifyMatches( testFig.Pointer, 'arrow', ...
['The mouse pointer did not change to ''arrow''', ...
' when moved over a button in a ', ConstructorName, ...
@@ -303,6 +376,7 @@ function tMousePointerUpdatesOnFlexChange( ...
dividerCenter = figureOrigin + ...
getpixelcenter( div2(end), true );
moveMouseTo( dividerCenter )
+ pause( 1 )
testCase.verifyMatches( testFig.Pointer, ...
'(left|right|top|bottom)', ...
['The mouse pointer did not change to ''left'', ', ...
@@ -311,6 +385,7 @@ function tMousePointerUpdatesOnFlexChange( ...
dividerCenter = figureOrigin + ...
getpixelcenter( div1(end), true );
moveMouseTo( dividerCenter )
+ pause( 1 )
testCase.verifyMatches( testFig.Pointer, ...
'(left|right|top|bottom)', ...
['The mouse pointer did not change to ''left'', ', ...
@@ -319,6 +394,7 @@ function tMousePointerUpdatesOnFlexChange( ...
buttonCenter = figureOrigin + ...
getpixelcenter( buttons1(1), true );
moveMouseTo( buttonCenter )
+ pause( 1 )
testCase.verifyEqual( testFig.Pointer, 'arrow', ...
['The mouse pointer did not change to ''arrow''', ...
' when moved over a button in a ', ConstructorName, ...
@@ -329,9 +405,18 @@ function tMousePointerUpdatesOnFlexChange( ...
function tMousePointerUpdatesOverDivider( ...
testCase, ConstructorName )
- % This test is only for rooted components.
+ % This test is only for rooted components in the JavaScript
+ % Desktop.
testCase.assumeGraphicsAreRooted()
+ % If running in CI, assume we have at least R2023b and we're
+ % running in the JavaScript desktop.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci ) && strcmp( ci, 'true' )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2023b' )
+ testCase.assumeJavaScriptDesktop()
+ end % if
+
% Create the layout and add children.
[component, dividers] = createFlexibleLayoutWithChildren( ...
testCase, ConstructorName );
@@ -341,7 +426,10 @@ function tMousePointerUpdatesOverDivider( ...
figureOrigin = getFigureOrigin( testFig );
dividerCenter = figureOrigin + ...
getpixelcenter( dividers(1), true );
+ moveMouseTo( dividerCenter - [10, 10] )
+ pause( 0.5 )
moveMouseTo( dividerCenter )
+ pause( 0.5 )
testCase.verifyMatches( testFig.Pointer, ...
'(left|right|top|bottom)', ...
['The mouse pointer did not change to ''left'', ', ...
@@ -350,50 +438,91 @@ function tMousePointerUpdatesOverDivider( ...
end % tMousePointerUpdatesOverDivider
- function tClickingDividerIsWarningFree( testCase, ConstructorName )
+ function tDeletingChildRestoresPointer( testCase, ConstructorName )
- % This test is only for rooted components.
+ % This test is for rooted components.
testCase.assumeGraphicsAreRooted()
- % Create the layout and add children.
- [component, dividers] = createFlexibleLayoutWithChildren( ...
- testCase, ConstructorName );
+ % If running in CI, assume we have at least R2023b.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci ) && strcmp( ci, 'true' )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2023b' )
+ end % if
- % Move the mouse to the center of a divider.
+ % Create a component with children.
+ [component, dividers] = testCase...
+ .createFlexibleLayoutWithChildren( ConstructorName );
+
+ % Increase the spacing.
+ component.Spacing = 10;
+
+ % Move the mouse over a divider.
+ r = groot();
testFig = ancestor( component, 'figure' );
- figureOrigin = getFigureOrigin( testFig );
- dividerCenter = figureOrigin + ...
- getpixelcenter( dividers(1), true );
- moveMouseTo( dividerCenter )
+ r.PointerLocation = testFig.Position(1:2) + ...
+ dividers(1).Position(1:2);
+ pause( 0.5 )
- % Verify that clicking the divider is warning-free.
- testCase.verifyWarningFree( @clicker, ...
- ['Clicking the divider in a ', ConstructorName, ...
- ' component was not warning-free.'] )
+ % Delete all the children.
+ delete( component.Children )
+ pause( 0.5 )
- function clicker()
+ % Verify that the figure's 'Pointer' property has been
+ % restored.
+ testCase.verifyEqual( testFig.Pointer, 'arrow', ...
+ ['Deleting the children of a ', ConstructorName, ...
+ ' component did not restore the figure''s ', ...
+ '''Pointer'' property.'] )
- % Create the robot.
- bot = java.awt.Robot();
+ end % tDeletingChildRestoresPointer
- % Click.
- bot.mousePress( java.awt.event.InputEvent.BUTTON1_MASK );
- pause( 0.5 )
+ function tReparentingLayoutRestoresPointer( ...
+ testCase, ConstructorName )
- % Let go.
- bot.mouseRelease( java.awt.event.InputEvent.BUTTON1_MASK );
- pause( 0.5 )
+ % This test is for rooted components.
+ testCase.assumeGraphicsAreRooted()
+ % If running in CI, assume we have the JavaScript desktop.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci ) && strcmp( ci, 'true' )
+ testCase.assumeJavaScriptDesktop()
+ end % if
- end % clicker
+ % Create a component with children.
+ [component, dividers] = testCase...
+ .createFlexibleLayoutWithChildren( ConstructorName );
- end % tClickingDividerIsWarningFree
+ % Increase the spacing.
+ component.Spacing = 10;
+
+ % Move the mouse over a divider.
+ r = groot();
+ testFig = ancestor( component, 'figure' );
+ r.PointerLocation = testFig.Position(1:2) + ...
+ dividers(1).Position(1:2);
+ pause( 0.5 )
+
+ % Reparent the layout.
+ component.Parent = [];
+
+ % Verify that the figure's 'Pointer' property has been
+ % restored.
+ testCase.verifyEqual( testFig.Pointer, 'arrow', ...
+ ['Reparenting a ', ConstructorName, ...
+ ' component did not restore the figure''s ', ...
+ '''Pointer'' property.'] )
+
+ end % tReparentingLayoutRestoresPointer
+
+ end % methods ( Test, Sealed, TestTags = {'MovesMouse'} )
+
+ methods ( Test, Sealed )
function tSettingBackgroundColorUpdatesDividers( ...
testCase, ConstructorName )
% Create the layout and add children.
[component, dividers] = createFlexibleLayoutWithChildren( ...
- testCase, ConstructorName );
+ testCase, ConstructorName );
% Set the background color.
newColor = [1, 0, 0];
@@ -417,7 +546,7 @@ function tTurningOffDividerMarkingsSetsDividerMarkingsProperty( ...
% Create the layout and add children.
[component, dividers] = createFlexibleLayoutWithChildren( ...
- testCase, ConstructorName );
+ testCase, ConstructorName );
% Switch off the divider markings.
component.DividerMarkings = 'off';
@@ -453,70 +582,6 @@ function tReparentingToEmptyFigureIsWarningFree( ...
end % tReparentingToEmptyFigureIsWarningFree
- function tDeletingChildRestoresPointer( testCase, ConstructorName )
-
- % This test is for rooted components.
- testCase.assumeGraphicsAreRooted()
-
- % Create a component with children.
- [component, dividers] = testCase...
- .createFlexibleLayoutWithChildren( ConstructorName );
-
- % Increase the spacing.
- component.Spacing = 10;
-
- % Move the mouse over a divider.
- r = groot();
- testFig = ancestor( component, 'figure' );
- r.PointerLocation = testFig.Position(1:2) + ...
- dividers(1).Position(1:2);
- pause( 0.5 )
-
- % Delete all the children.
- delete( component.Children )
- pause( 0.5 )
-
- % Verify that the figure's 'Pointer' property has been
- % restored.
- testCase.verifyEqual( testFig.Pointer, 'arrow', ...
- ['Deleting the children of a ', ConstructorName, ...
- ' component did not restore the figure''s ', ...
- '''Pointer'' property.'] )
-
- end % tDeletingChildRestoresPointer
-
- function tReparentingLayoutRestoresPointer( ...
- testCase, ConstructorName )
-
- % This test is for rooted components.
- testCase.assumeGraphicsAreRooted()
-
- % Create a component with children.
- [component, dividers] = testCase...
- .createFlexibleLayoutWithChildren( ConstructorName );
-
- % Increase the spacing.
- component.Spacing = 10;
-
- % Move the mouse over a divider.
- r = groot();
- testFig = ancestor( component, 'figure' );
- r.PointerLocation = testFig.Position(1:2) + ...
- dividers(1).Position(1:2);
- pause( 0.5 )
-
- % Reparent the layout.
- component.Parent = [];
-
- % Verify that the figure's 'Pointer' property has been
- % restored.
- testCase.verifyEqual( testFig.Pointer, 'arrow', ...
- ['Reparenting a ', ConstructorName, ...
- ' component did not restore the figure''s ', ...
- '''Pointer'' property.'] )
-
- end % tReparentingLayoutRestoresPointer
-
function tStringSupportForDividerMarkings( ...
testCase, ConstructorName )
@@ -537,7 +602,7 @@ function tStringSupportForDividerMarkings( ...
end % tStringSupportForDividerMarkings
- end % methods ( Test )
+ end % methods ( Test, Sealed )
methods ( Access = private )
diff --git a/tests/runToolboxTests.m b/tests/runToolboxTests.m
index 625e2bdc..65988dc3 100644
--- a/tests/runToolboxTests.m
+++ b/tests/runToolboxTests.m
@@ -1,7 +1,20 @@
-function results = runToolboxTests()
+function results = runToolboxTests( namedArgs )
%RUNTOOLBOXTESTS Run the GUI Layout Toolbox tests.
+%
+% results = runToolboxTests() runs all GLT tests and returns the results.
+%
+% results = runToolboxTests( 'ExcludeMouseTests', true ) excludes tests
+% that use either the Java robot or MATLAB to perform mouse interactions
+% and returns the results.
+%
+% results = runToolboxTests( 'ExcludeMouseTests', false ) runs all GLT
+% tests and returns the results.
-% Identify the current folder.
+arguments
+ namedArgs.ExcludeMouseTests(1, 1) logical = false
+end % arguments
+
+% Record the current folder (the tests directory).
rootFolder = fileparts( mfilename( 'fullpath' ) );
% Disable the warning about name conflicts.
@@ -10,9 +23,23 @@
warningCleanup = onCleanup( @() warning( w ) );
warning( 'off', ID )
-% Run the tests.
-results = runtests( rootFolder, ...
+% Create the test suite, including tests in subfolders and subpackages.
+suite = testsuite( rootFolder, ...
'IncludeSubfolders', true, ...
'IncludeSubpackages', true );
+% Filter the test suite using the user-specified parameters. This
+% determines the tests to exclude based on their tags.
+if namedArgs.ExcludeMouseTests
+ suiteIdx = 1 : length( suite );
+ filterFun = @( idx ) ~isempty( suite(idx).Tags ) && ...
+ all( strcmp( suite(idx).Tags, 'MovesMouse' ) );
+ excludeIdx = arrayfun( filterFun, suiteIdx );
+ suite(excludeIdx) = [];
+end % if
+
+% Run the tests, recording text output.
+runner = matlab.unittest.TestRunner.withTextOutput();
+results = runner.run( suite );
+
end % runToolboxTests
\ No newline at end of file
diff --git a/tests/tBoxPanel.m b/tests/tBoxPanel.m
index a7d995ae..827f0e5e 100644
--- a/tests/tBoxPanel.m
+++ b/tests/tBoxPanel.m
@@ -8,22 +8,20 @@
% constructor and get/set methods.
NameValuePairs = {{
'BackgroundColor', [1, 1, 0], ...
- 'BorderType', 'line', ...
- 'BorderWidth', 2, ...
+ 'BorderType', 'line', ...
'CloseRequestFcn', @glttestutilities.noop, ...
'CloseTooltipString', 'Close', ...
'DeleteFcn', @glttestutilities.noop, ...
'DockFcn', @glttestutilities.noop, ...
'DockTooltipString', 'Dock', ...
'FontAngle', 'italic', ...
- 'FontName', 'SansSerif', ...
+ 'FontName', 'Helvetica', ...
'FontUnits', 'pixels', ...
'FontSize', 20, ...
'FontWeight', 'bold', ...
'ForegroundColor', [0, 0, 1], ...
'HelpFcn', @glttestutilities.noop, ...
- 'HelpTooltipString', 'Help', ...
- 'HighlightColor', [1, 0, 1], ...
+ 'HelpTooltipString', 'Help', ...
'IsDocked', false, ...
'IsMinimized', false, ...
'MaximizeTooltipString', 'Maximize', ...
@@ -40,8 +38,7 @@
}, ...
{
'BackgroundColor', [1, 1, 0], ...
- 'BorderType', 'line', ...
- 'BorderWidth', 2, ...
+ 'BorderType', 'line', ...
'CloseRequestFcn', @glttestutilities.noop, ...
'CloseTooltipString', 'Close', ...
'DeleteFcn', @glttestutilities.noop, ...
@@ -49,14 +46,13 @@
'DockFcn', @glttestutilities.noop, ...
'DockTooltipString', 'Dock', ...
'FontAngle', 'italic', ...
- 'FontName', 'SansSerif', ...
+ 'FontName', 'Helvetica', ...
'FontUnits', 'pixels', ...
'FontSize', 20, ...
'FontWeight', 'bold', ...
'ForegroundColor', [0, 0, 1], ...
'HelpFcn', @glttestutilities.noop, ...
- 'HelpTooltipString', 'Help', ...
- 'HighlightColor', [1, 0, 1], ...
+ 'HelpTooltipString', 'Help', ...
'MaximizeTooltipString', 'Maximize', ...
'Minimized', false, ...
'MinimizeFcn', @glttestutilities.noop, ...
diff --git a/tests/tExamples.m b/tests/tExamples.m
index 727114dd..55a6d57e 100644
--- a/tests/tExamples.m
+++ b/tests/tExamples.m
@@ -2,28 +2,22 @@
%tExamples Tests for the layout documentation examples.
properties ( TestParameter )
- % Example script names.
- ScriptFile = {'axesexample', ...
- 'colorbarexample', ...
- 'gridflexpositioning', ...
- 'hierarchyexample', ...
- 'paneltabexample', ...
- 'visibleexample'}
- % Variables representing the main figure/app window in each
- % example.
- FigureVariable = {'window', 'window', 'f', ...
- 'window', 'window', 'fig'}
+ % Example script names and corresponding figure variables.
+ ScriptFile = {{'axesexample', 'window'}; ...
+ {'colorbarexample', 'window'}; ...
+ {'gridflexpositioning', 'f'}; ...
+ {'hierarchyexample', 'window'}; ...
+ {'paneltabexample', 'window'}; ...
+ {'visibleexample', 'fig'}}
end % properties ( TestParameter )
properties ( TestParameter )
- % Example function names.
+ % Example function names and corresponding figure variables.
FunctionFile = {'callbackexample', ...
'demoBrowser', ...
- 'dockexample', ...
+ 'dockexample', ...
+ 'guideApp', ...
'minimizeexample'}
- % Variables representing the main figure/app window in each
- % example.
- OutputVariable = {'f', 'gui', 'fig', 'fig'}
end % properties ( TestParameter )
methods ( TestClassSetup )
@@ -36,19 +30,22 @@ function addDocumentationFoldersToPath( testCase )
foldersToAdd = {fullfile( projectFolder, 'docsrc' ), ...
fullfile( projectFolder, 'docsrc', 'Examples' )};
- % Apply a path fixture for these folders.
- pathFixture = matlab.unittest.fixtures...
- .PathFixture( foldersToAdd );
- testCase.applyFixture( pathFixture )
+ % Apply path fixtures for these folders (a loop is needed
+ % because the PathFixture is not vectorized until R2021a).
+ for k = 1 : numel( foldersToAdd )
+ pathFixture = matlab.unittest.fixtures...
+ .PathFixture( foldersToAdd{k} );
+ testCase.applyFixture( pathFixture )
+ end % for
end % addDocumentationFoldersToPath
end % methods ( TestClassSetup )
- methods ( Test, Sealed, ParameterCombination = 'sequential' )
+ methods ( Test, Sealed )
function tRunningExampleScriptIsWarningFree( ...
- testCase, ScriptFile, FigureVariable )
+ testCase, ScriptFile )
% Do not repeat this test for each parent type.
testCase.assumeComponentHasEmptyParent()
@@ -69,18 +66,18 @@ function tRunningExampleScriptIsWarningFree( ...
testCase.addTeardown( @() fclose( fileID ) );
% Read the example contents.
- exampleContent = fileread( [ScriptFile, '.m'] );
+ exampleContent = fileread( [ScriptFile{1}, '.m'] );
% Write a wrapper function to the temporary file, providing an
% output using the output variable name.
fprintf( fileID, 'function %s = %s()\n\n', ...
- FigureVariable, tempFilename );
+ ScriptFile{2}, tempFilename );
fprintf( fileID, '%s', exampleContent );
% Verify that running the wrapper function is warning-free.
runner = @() exampleRunner( tempFilename );
testCase.verifyWarningFree( runner, ['Running the ', ...
- ScriptFile, ' example was not warning-free.'] )
+ ScriptFile{1}, ' example was not warning-free.'] )
function exampleRunner( file )
@@ -91,72 +88,24 @@ function exampleRunner( file )
end % tRunningExampleScriptIsWarningFree
- function tGuideAppIsWarningFree( testCase )
-
- testCase.verifyWarningFree( @guideAppRunner, ...
- ['Running the guideApp documentation example ', ...
- 'was not warning-free.'] )
-
- function guideAppRunner()
-
- f = guideApp();
- testCase.addTeardown( @() delete( f ) )
-
- end % guideAppRunner
-
- end % tGuideAppIsWarningFree
-
- function tRunningExampleFunctionIsWarningFree( ...
- testCase, FunctionFile, OutputVariable )
+ function tExampleFunctionIsWarningFree( testCase, FunctionFile )
% Do not repeat this test for each parent type.
testCase.assumeComponentHasEmptyParent()
- % Assume that we are in MATLAB R2016a or later.
- testCase.assumeMATLABVersionIsAtLeast( 'R2016a' )
+ % Verify that launching the example is warning-free.
+ testCase.verifyWarningFree( @appRunner, ...
+ ['Running the ', FunctionFile, ' example was not ', ...
+ 'warning-free.'])
- % Create a working folder fixture.
- tempFolderFixture = matlab.unittest.fixtures...
- .WorkingFolderFixture();
- testCase.applyFixture( tempFolderFixture )
+ function appRunner()
- % Create a temporary file.
- [~, tempFilename] = fileparts( tempname );
- tempFullFilename = fullfile( ...
- tempFolderFixture.Folder, [tempFilename, '.m'] );
- fileID = fopen( tempFullFilename, 'w' );
- testCase.addTeardown( @() fclose( fileID ) );
-
- % Read the example contents.
- exampleContent = fileread( [FunctionFile, '.m'] );
-
- % Remove the function definition line.
- exampleContent = strsplit( exampleContent, '\n' );
- exampleContent = [exampleContent{2:end}];
-
- % Write a wrapper function to the temporary file, providing an
- % output using the output variable name.
- fprintf( fileID, 'function %s = %s()\n\n', ...
- OutputVariable, tempFilename );
- fprintf( fileID, '%s', exampleContent );
-
- % Verify that running the wrapper function is warning-free.
- runner = @() exampleRunner( tempFilename );
- testCase.verifyWarningFree( runner, ['Running the ', ...
- FunctionFile, ' example was not warning-free.'] )
-
- function exampleRunner( file )
-
- fig = feval( file );
- if strcmp( FunctionFile, 'demoBrowser' )
- testCase.addTeardown( @() delete( fig.Window ) )
- else
- testCase.addTeardown( @() delete( fig ) )
- end % if
+ fig = feval( FunctionFile );
+ testCase.addTeardown( @() delete( fig ) )
- end % exampleRunner
+ end % appRunner
- end % tRunningExampleFunctionIsWarningFree
+ end % tExampleFunctionIsWarningFree
end % methods ( Test, Sealed )
diff --git a/tests/tGridFlex.m b/tests/tGridFlex.m
index dfc89aa2..0f40b227 100644
--- a/tests/tGridFlex.m
+++ b/tests/tGridFlex.m
@@ -60,7 +60,7 @@
}}
end % properties ( TestParameter )
- methods ( Test, Sealed )
+ methods ( Test, Sealed, TestTags = {'MovesMouse'} )
function tDraggingRowDividerIsWarningFree( ...
testCase, ConstructorName, ChildrenSizes )
@@ -68,6 +68,12 @@ function tDraggingRowDividerIsWarningFree( ...
% Assume that the graphics are rooted.
testCase.assumeGraphicsAreRooted()
+ % If running in CI, assume we have at least R2023b.
+ ci = getenv( 'GITHUB_ACTIONS' );
+ if ~isempty( ci ) && strcmp( ci, 'true' )
+ testCase.assumeMATLABVersionIsAtLeast( 'R2023b' )
+ end % if
+
% Create a component.
component = testCase.constructComponent( ConstructorName, ...
'Spacing', 10 );
@@ -104,7 +110,7 @@ function tDraggingRowDividerIsWarningFree( ...
% Drag the divider in both directions.
for offset = dragOffsets
- % Move the mouse pointer.
+ % Move the mouse pointer.
r.PointerLocation = testFig.Position(1:2) + ...
d(1).Position(1:2) + d(1).Position(3:4)/2;
drawnow()
@@ -140,6 +146,6 @@ function dragger( offset )
end % tDraggingRowDividerIsWarningFree
- end % methods ( Test, Sealed )
+ end % methods ( Test, Sealed, TestTags = {'MovesMouse'} )
end % class
\ No newline at end of file
diff --git a/tests/tPanel.m b/tests/tPanel.m
index 7fd1b6fb..389a0cc9 100644
--- a/tests/tPanel.m
+++ b/tests/tPanel.m
@@ -21,9 +21,9 @@
'DeleteFcn', @glttestutilities.noop, ...
'Enable', 'on', ...
'FontAngle', 'normal', ...
- 'FontName', 'SansSerif', ...
- 'FontSize', 20, ...
- 'FontUnits', 'points', ...
+ 'FontName', 'Monospaced', ...
+ 'FontUnits', 'pixels', ...
+ 'FontSize', 20, ...
'FontWeight', 'bold', ...
'ForegroundColor', [0, 0, 0], ...
'HighlightColor', [1, 1, 1], ...
diff --git a/tests/tScrollingPanel.m b/tests/tScrollingPanel.m
index cf6030c8..000530c2 100644
--- a/tests/tScrollingPanel.m
+++ b/tests/tScrollingPanel.m
@@ -78,23 +78,20 @@ function tContentsPositionIsFullWhenPanelIsResized( ...
expectedPosition = [1, 1, scrollPanel.Position(3:4)];
testCase.verifyEqual( c.Position, expectedPosition, ...
['Adding a child to ', ConstructorName, ' did not ', ...
- 'set the child''s ''Position'' property correctly.'] )
-
- % Change the dimensions of the scrolling panel.
- scrollPanelPos = scrollPanel.Position;
- for k = 1 : 8
- % Update the 'Position' property of the scrolling panel.
- newDims = 50 * [sin( pi*k/8 ), cos( pi*k/8 )];
- scrollPanel.Position = scrollPanelPos + [0, 0, newDims];
- drawnow()
- % Verify that the child still fills the scroll panel.
- expectedPosition = [1, 1, scrollPanel.Position(3:4)];
- testCase.verifyEqual( c.Position, expectedPosition, ...
- 'AbsTol', 1e-10, ...
- ['Changing the dimensions of the scrolling ', ...
- 'panel did not update the ''Position'' property ', ...
- 'of its contents correctly.'] )
- end % for
+ 'set the child''s ''Position'' property correctly.'] )
+
+ % Update the 'Position' property of the scrolling panel.
+ newDims = [0, -50];
+ scrollPanel.Position = scrollPanel.Position + [0, 0, newDims];
+ drawnow()
+
+ % Verify that the child still fills the scroll panel.
+ expectedPosition = [1, 1, scrollPanel.Position(3:4)];
+ testCase.verifyEqual( c.Position, expectedPosition, ...
+ 'AbsTol', 1e-10, ...
+ ['Changing the dimensions of the scrolling ', ...
+ 'panel did not update the ''Position'' property ', ...
+ 'of its contents correctly.'] )
end % tContentsPositionIsFullWhenPanelIsResized
diff --git a/tests/tTabPanel.m b/tests/tTabPanel.m
index 849ed2bb..85dde042 100644
--- a/tests/tTabPanel.m
+++ b/tests/tTabPanel.m
@@ -13,7 +13,7 @@
'Tag', 'Test', ...
'Visible', 'on', ...
'FontAngle', 'italic', ...
- 'FontName', 'SansSerif', ...
+ 'FontName', 'Helvetica', ...
'FontUnits', 'centimeters', ...
'FontSize', 0.5, ...
'FontWeight', 'bold', ...
@@ -33,7 +33,7 @@
'Tag', 'Test', ...
'Visible', 'on', ...
'FontAngle', 'italic', ...
- 'FontName', 'SansSerif', ...
+ 'FontName', 'Helvetica', ...
'FontUnits', 'centimeters', ...
'FontSize', 0.5, ...
'FontWeight', 'bold', ...
@@ -579,6 +579,112 @@ function tAddingAxesToDisabledTabHidesContents( ...
end % tAddingAxesToDisabledTabHidesContents
+ function tStringSupportForTabPanelScalarStringProperties( ...
+ testCase, ConstructorName )
+
+ % Assume that we are in R2017b or later.
+ testCase.assumeMATLABVersionIsAtLeast( 'R2017b' )
+
+ % Construct a tab panel.
+ tabPanel = testCase.constructComponent( ConstructorName );
+
+ % Define a list of property names and values to set.
+ propertyNames = {'FontAngle', 'FontName', 'FontWeight', ...
+ 'FontUnits', 'TabLocation'};
+ propertyValues = string( {'italic', 'Helvetica', 'bold', ...
+ 'pixels', 'bottom'} ); %#ok
+
+ for k = 1 : numel( propertyNames )
+ currentProperty = propertyNames{k};
+ currentValue = propertyValues(k);
+ % Set the property, using a string value.
+ tabPanel.(currentProperty) = currentValue;
+ % Verify that the correct value has been stored.
+ testCase.verifyEqual( tabPanel.(currentProperty), ...
+ char( currentValue ), ...
+ ['The ''', currentProperty, ''' property of the ', ...
+ ConstructorName, ...
+ ' component did not accept a string value.'] )
+ end % for
+
+ end % tStringSupportForTabPanelScalarStringProperties
+
+ function tStringSupportForTabEnablesProperty( ...
+ testCase, ConstructorName )
+
+ % Assume that we are in R2017b or later.
+ testCase.assumeMATLABVersionIsAtLeast( 'R2017b' )
+
+ % Construct a tab panel.
+ tabPanel = testCase.constructComponent( ConstructorName );
+
+ % Add one child.
+ uicontrol( tabPanel )
+
+ % Set the TabEnables property.
+ tabEnables = string( 'off' ); %#ok
+ tabPanel.TabEnables = tabEnables;
+ testCase.verifyEqual( tabPanel.TabEnables, ...
+ cellstr( tabEnables ), ...
+ ['The ''TabEnables'' property on the ', ...
+ ConstructorName, ' component (when it has one child) ', ...
+ 'did not accept a string value.'] )
+
+ % Add further children.
+ for k = 1 : 3
+ uicontrol( tabPanel )
+ end % for
+
+ % Set the TabEnables property.
+ tabEnables = string( {'on'; 'off'; 'on'; 'off'} ); %#ok
+ tabPanel.TabEnables = tabEnables;
+
+ % Verify that the stored value is correct.
+ testCase.verifyEqual( tabPanel.TabEnables, ...
+ cellstr( tabEnables ), ['The ''TabEnables'' property ', ...
+ ' of the ', ConstructorName, ' component (when it has', ...
+ ' multiple children) did not accept a string value.'] )
+
+ end % tStringSupportForTabEnablesProperty
+
+ function tStringSupportForTabTitlesProperty( ...
+ testCase, ConstructorName )
+
+ % Assume that we are in R2017b or later.
+ testCase.assumeMATLABVersionIsAtLeast( 'R2017b' )
+
+ % Construct a tab panel.
+ tabPanel = testCase.constructComponent( ConstructorName );
+
+ % Add one child.
+ uicontrol( tabPanel )
+
+ % Set the TabTitles property.
+ tabTitles = string( 'My Title' ); %#ok
+ tabPanel.TabTitles = tabTitles;
+ testCase.verifyEqual( tabPanel.TabTitles, ...
+ cellstr( tabTitles ), ...
+ ['The ''TabTitles'' property on the ', ...
+ ConstructorName, ' component (when it has one child) ', ...
+ 'did not accept a string value.'] )
+
+ % Add further children.
+ for k = 1 : 3
+ uicontrol( tabPanel )
+ end % for
+
+ % Set the TabTitles property.
+ tabTitles = string( {'A'; 'B'; 'C'; 'D'} ); %#ok
+ tabPanel.TabTitles = tabTitles;
+
+ % Verify that the stored value is correct.
+ testCase.verifyEqual( tabPanel.TabTitles, ...
+ cellstr( tabTitles ), ['The ''TabTitles'' property ', ...
+ ' of the ', ConstructorName, ' component (when it has', ...
+ ' multiple children) did not accept a string value.'] )
+
+ end % tStringSupportForTabTitlesProperty
+
end % methods ( Test, Sealed )
methods ( Access = private )