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 )