diff --git a/.circleci/config.yml b/.circleci/config.yml index edb7bb1c496..0dcca1d322b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -160,6 +160,31 @@ jobs: equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 steps: - generate_and_store_version_and_filesystem_artifacts + e2e-mobile: + executor: pw-focal-development + steps: + - build_and_install: + node-version: lts/hydrogen + - run: npm run test:e2e:mobile + - when: + condition: + equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + steps: + - generate_e2e_code_cov_report: + suite: full + - store_test_results: + path: test-results/results.xml + - store_artifacts: + path: test-results + - store_artifacts: + path: coverage + - store_artifacts: + path: html-test-results + - when: + condition: + equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + steps: + - generate_and_store_version_and_filesystem_artifacts e2e-couchdb: executor: ubuntu steps: @@ -260,8 +285,9 @@ workflows: - e2e-test: name: e2e-stable suite: stable + - e2e-mobile - visual-a11y-tests: - name: visual-test-ci + name: visual-a11y-test-ci suite: ci the-nightly: #These jobs do not run on PRs, but against master at night @@ -277,10 +303,11 @@ workflows: - e2e-test: name: e2e-full-nightly suite: full - - mem-test + - e2e-mobile - perf-test + - mem-test - visual-a11y-tests: - name: visual-test-nightly + name: visual-a11y-test-nightly suite: full - e2e-couchdb triggers: diff --git a/e2e/README.md b/e2e/README.md index 83840a612e2..ee0debe9dd6 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -229,7 +229,7 @@ Current list of test tags: |Test Tag|Description| |:-:|-| -|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).| +|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).| |`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.| |`@gds` | Denotes a GDS Test Case used in the VIPER Mission.| |`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.| @@ -329,9 +329,15 @@ In terms of operating system testing, we're only limited by what the CI provider #### **Mobile** -We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project. +We have a Mission-need to support iPad and mobile devices. To run our test suites with mobile devices, please see our `playwright-mobile.config.js` projects. -In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button) and so this will likely turn into a separate suite. +In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage. + +For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the +```sh +npm run test:e2e:mobile +``` +command. #### **Skipping or executing tests based on browser, os, and/os browser version:** diff --git a/e2e/playwright-ci.config.js b/e2e/playwright-ci.config.js index 8c5c906bd5c..d86fd2507e0 100644 --- a/e2e/playwright-ci.config.js +++ b/e2e/playwright-ci.config.js @@ -10,6 +10,7 @@ const NUM_WORKERS = 2; const config = { retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite testDir: 'tests', + grepInvert: /@mobile/, //Ignore mobile tests testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js timeout: 60 * 1000, webServer: { diff --git a/e2e/playwright-local.config.js b/e2e/playwright-local.config.js index c90dd174965..37afca21529 100644 --- a/e2e/playwright-local.config.js +++ b/e2e/playwright-local.config.js @@ -1,13 +1,11 @@ // playwright.config.js // @ts-check -// eslint-disable-next-line no-unused-vars -import { devices } from '@playwright/test'; - /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { retries: 0, testDir: 'tests', + testMatch: '**/*.e2e.spec.js', // only run e2e tests testIgnore: '**/*.perf.spec.js', timeout: 30 * 1000, webServer: { @@ -35,7 +33,6 @@ const config = { }, { name: 'MMOC', - testMatch: '**/*.e2e.spec.js', // only run e2e tests grepInvert: /@snapshot/, use: { browserName: 'chromium', @@ -47,8 +44,6 @@ const config = { }, { name: 'safari', - testMatch: '**/*.e2e.spec.js', // only run e2e tests - grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340 grepInvert: /@snapshot/, use: { browserName: 'webkit' @@ -56,7 +51,6 @@ const config = { }, { name: 'firefox', - testMatch: '**/*.e2e.spec.js', // only run e2e tests grepInvert: /@snapshot/, use: { browserName: 'firefox' @@ -64,7 +58,6 @@ const config = { }, { name: 'canary', - testMatch: '**/*.e2e.spec.js', // only run e2e tests grepInvert: /@snapshot/, use: { browserName: 'chromium', @@ -73,22 +66,11 @@ const config = { }, { name: 'chrome-beta', - testMatch: '**/*.e2e.spec.js', // only run e2e tests grepInvert: /@snapshot/, use: { browserName: 'chromium', channel: 'chrome-beta' } - }, - { - name: 'ipad', - testMatch: '**/*.e2e.spec.js', // only run e2e tests - grep: /@ipad/, - grepInvert: /@snapshot/, - use: { - browserName: 'webkit', - ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json - } } ], reporter: [ diff --git a/e2e/playwright-mobile.config.js b/e2e/playwright-mobile.config.js new file mode 100644 index 00000000000..387b29eacaf --- /dev/null +++ b/e2e/playwright-mobile.config.js @@ -0,0 +1,69 @@ +// playwright.config.js +// @ts-check + +import { devices } from '@playwright/test'; +const MAX_FAILURES = 5; +const NUM_WORKERS = 2; + +import { fileURLToPath } from 'url'; + +/** @type {import('@playwright/test').PlaywrightTestConfig} */ +const config = { + retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite + testDir: 'tests', + testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js + timeout: 30 * 1000, + webServer: { + command: 'npm run start:coverage', + url: 'http://localhost:8080/#', + timeout: 200 * 1000, + reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging. + }, + maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste + workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent + use: { + baseURL: 'http://localhost:8080/', + headless: true, + ignoreHTTPSErrors: true, + screenshot: 'only-on-failure', + trace: 'on-first-retry', + video: 'off' + }, + projects: [ + { + name: 'ipad', + grep: /@mobile/, + use: { + storageState: fileURLToPath( + new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url) + ), + browserName: 'webkit', + ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json + } + }, + { + name: 'iphone', + grep: /@mobile/, + use: { + storageState: fileURLToPath( + new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url) + ), + browserName: 'webkit', + ...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json + } + } + ], + reporter: [ + ['list'], + [ + 'html', + { + open: 'never', + outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 + } + ], + ['junit', { outputFile: '../test-results/results.xml' }] + ] +}; + +export default config; diff --git a/e2e/playwright-watch.config.js b/e2e/playwright-watch.config.js index 1d65e655595..bd40be46388 100644 --- a/e2e/playwright-watch.config.js +++ b/e2e/playwright-watch.config.js @@ -1,6 +1,9 @@ // playwright.config.js // @ts-check +import { devices } from '@playwright/test'; +import { fileURLToPath } from 'url'; + /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { retries: 0, //Retries are not needed with watch mode @@ -28,6 +31,28 @@ const config = { use: { browserName: 'chromium' } + }, + { + name: 'ipad', + grep: /@mobile/, + use: { + storageState: fileURLToPath( + new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url) + ), + browserName: 'webkit', + ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json + } + }, + { + name: 'iphone', + grep: /@mobile/, + use: { + storageState: fileURLToPath( + new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url) + ), + browserName: 'webkit', + ...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json + } } ], reporter: [ diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 3a1931cd154..19b3ff526bf 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -363,7 +363,7 @@ test.describe('Example Imagery in Display Layout', () => { await page.locator('li[title="View Large"]').click(); await expect(pausePlayButton).toHaveClass(/is-paused/); - await page.locator('[aria-label="Close"]').click(); + await page.getByRole('button', { name: 'Close' }).click(); await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); }); @@ -386,7 +386,7 @@ test.describe('Example Imagery in Display Layout', () => { await page.locator('li[title="View Large"]').click(); await expect(pausePlayButton).toHaveClass(/is-paused/); - await page.locator('[aria-label="Close"]').click(); + await page.getByRole('button', { name: 'Close' }).click(); await expect.soft(pausePlayButton).toHaveClass(/is-paused/); }); @@ -509,7 +509,7 @@ test.describe('Example Imagery in Flexible layout', () => { await page.getByRole('button', { name: 'Background Image', state: 'visible' }); // Close the large view - await page.getByLabel('Close').click(); + await page.getByRole('button', { name: 'Close' }).click(); }); test.beforeEach(async ({ page }) => { diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js index c6f4f902e88..15fd97b214d 100644 --- a/e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookSnapshotImage.e2e.spec.js @@ -68,7 +68,7 @@ test.describe('Snapshot image tests', () => { // expect large image to be displayed await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible(); - await page.getByLabel('Close').click(); + await page.getByRole('button', { name: 'Close' }).click(); // drop another image onto the entry await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer }); diff --git a/e2e/tests/functional/plugins/plot/previews.e2e.spec.js b/e2e/tests/functional/plugins/plot/previews.e2e.spec.js index fc7f11a5a73..5d6211c3df7 100644 --- a/e2e/tests/functional/plugins/plot/previews.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/previews.e2e.spec.js @@ -57,7 +57,7 @@ test.describe('Plots work in Previews', () => { await page.getByLabel('Sine', { exact: true }).click({ button: 'right' }); await page.getByLabel('View Historical Data').click(); await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible(); - await page.getByLabel('Close').click(); + await page.getByRole('button', { name: 'Close' }).click(); await page.getByLabel('Expand Test Display Layout layout').click(); // change to a plot and ensure embiggen works @@ -73,7 +73,7 @@ test.describe('Plots work in Previews', () => { await expect(page.getByLabel('Preview Container')).toBeHidden(); await page.getByLabel('Large View').click(); await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible(); - await page.getByLabel('Close').click(); + await page.getByRole('button', { name: 'Close' }).click(); // get last sinewave tree item (in the display layout) await page @@ -83,6 +83,6 @@ test.describe('Plots work in Previews', () => { .click({ button: 'right' }); await page.getByLabel('View', { exact: true }).click(); await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible(); - await page.getByLabel('Close').click(); + await page.getByRole('button', { name: 'Close' }).click(); }); }); diff --git a/e2e/tests/functional/plugins/telemetryTable/preview.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/preview.e2e.spec.js index e7d98f38a05..fe7a823f8f3 100644 --- a/e2e/tests/functional/plugins/telemetryTable/preview.e2e.spec.js +++ b/e2e/tests/functional/plugins/telemetryTable/preview.e2e.spec.js @@ -53,7 +53,7 @@ test.describe('Preview mode', () => { await expect(page.getByLabel('Export Table Data')).toBeVisible(); await expect(page.getByLabel('Export Marked Rows')).toBeVisible(); await page.getByRole('menuitem', { name: 'Pause' }).click(); - await page.getByLabel('Close').click(); + await page.getByRole('button', { name: 'Close' }).click(); await expandEntireTree(page); diff --git a/e2e/tests/functional/smoke.e2e.spec.js b/e2e/tests/functional/smoke.e2e.spec.js index a113c05afc6..316edba50ee 100644 --- a/e2e/tests/functional/smoke.e2e.spec.js +++ b/e2e/tests/functional/smoke.e2e.spec.js @@ -48,7 +48,7 @@ test('Verify that the create button appears and that the Folder Domain Object is await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); }); -test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => { +test('Verify that My Items Tree appears', async ({ page, openmctConfig }) => { const { myItemsFolderName } = openmctConfig; //Go to baseURL await page.goto('./'); diff --git a/e2e/tests/mobile/smoke.e2e.spec.js b/e2e/tests/mobile/smoke.e2e.spec.js new file mode 100644 index 00000000000..01bce7fb6b6 --- /dev/null +++ b/e2e/tests/mobile/smoke.e2e.spec.js @@ -0,0 +1,59 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which can quickly verify that any openmct installation is +operable and that any type of testing can proceed. + +Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them +more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly +as they cover a very "thin surface" of functionality. + +When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel +comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects. +Make no assumptions about the order that elements appear in the DOM. +*/ + +import { expect, test } from '../../pluginFixtures.js'; + +test('Verify that My Items Tree appears @mobile', async ({ page, openmctConfig }) => { + const { myItemsFolderName } = openmctConfig; + //Go to baseURL + await page.goto('./'); + + //My Items to be visible + await expect(page.getByRole('treeitem', { name: `${myItemsFolderName}` })).toBeVisible(); +}); + +test('Verify that user can search @mobile', async ({ page }) => { + //For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json' + await page.goto('./'); + + await page.getByRole('searchbox', { name: 'Search Input' }).click(); + await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout'); + //Search Results appear in search modal + await expect(page.getByLabel('Object Results').getByText('Parent Display Layout')).toBeVisible(); + //Clicking on the search result takes you to the object + await page.getByLabel('Object Results').getByText('Parent Display Layout').click(); + await page.getByTitle('Collapse Browse Pane').click(); + await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible(); +}); diff --git a/package.json b/package.json index 0dce04bec4e..c31513eca74 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "test:debug": "KARMA_DEBUG=true karma start karma.conf.cjs", "test:e2e": "npx playwright test", "test:e2e:a11y": "npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep @a11y", + "test:e2e:mobile": "npx playwright test --config=e2e/playwright-mobile.config.js", "test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1", "test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"", "test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable", diff --git a/src/ui/components/search.scss b/src/ui/components/search.scss index 96f01763867..34b877ad27f 100644 --- a/src/ui/components/search.scss +++ b/src/ui/components/search.scss @@ -12,6 +12,9 @@ &:before { // Mag glass icon content: $glyph-icon-magnify; + body.mobile & { // Make search icon stand out in mobile + opacity: 1; + } } &__use-regex { @@ -47,15 +50,26 @@ display: none; order: 99; padding: 1px 0; + body.mobile & { + display: block; + } + } &.is-active { + body.mobile & { // In mobile, persist the expanded search bar instead of collapsing upon clicking away + background-color: rgba($colorHeadFg, 0.2) !important; + width: 50vw !important; + } .c-search__use-regex { margin-left: 0; } &:before { width: 0; + body.mobile & { + width: auto; + } } input[type='text'], diff --git a/src/ui/layout/layout.scss b/src/ui/layout/layout.scss index 19694152ded..98eb93fc6e4 100644 --- a/src/ui/layout/layout.scss +++ b/src/ui/layout/layout.scss @@ -171,7 +171,7 @@ } } - &__head, + //&__head, &__pane-inspector { body.mobile & { display: none; @@ -189,6 +189,18 @@ margin-bottom: $interiorMargin; // Needs some additional visual separation } + &__head { + body.mobile & { + .c-create-button, + .c-create-menu, + .c-indicator, + .c-icon-button { + // Making status area visible, but hiding some widgets for mobile. + display: none; + } + } + } + body.mobile & .l-shell__main-view-browse-bar { margin-left: $mobileMenuIconD; // Make room for the hamburger! .c-button[class*='__actions__edit'] { @@ -241,7 +253,20 @@ border-left: $brdr; align-items: start; $p: $interiorMarginSm; - padding-left: $p; padding-right: $p; + padding-left: $p; + padding-right: $p; + } + + &__create-button, + &__app-logo { + flex: 0 0 auto; + } + + &__create-button { + body.mobile & { + // Fixes weird gap in mobile status area + margin-right: 0px; + } } &__indicators { @@ -254,7 +279,6 @@ height: 24px; overflow: hidden; } - } /******************************* MAIN AREA */ @@ -355,14 +379,18 @@ .is-editing { .l-shell__main-container { $m: 3px; - box-shadow: $colorBodyBg 0 0 0 1px, $editUIAreaShdw; + box-shadow: + $colorBodyBg 0 0 0 1px, + $editUIAreaShdw; margin-left: $m; margin-right: $m; top: $shellToolBarH + $shellMainBrowseBarH + $interiorMarginLg !important; &[s-selected] { // Provide a clearer selection context articulation for the main edit area - box-shadow: $colorBodyBg 0 0 0 1px, $editUIAreaShdwSelected; + box-shadow: + $colorBodyBg 0 0 0 1px, + $editUIAreaShdwSelected; } } } diff --git a/src/ui/layout/search/SearchResultsDropDown.vue b/src/ui/layout/search/SearchResultsDropDown.vue index a7943a2eb9a..5ff1b39834a 100644 --- a/src/ui/layout/search/SearchResultsDropDown.vue +++ b/src/ui/layout/search/SearchResultsDropDown.vue @@ -27,6 +27,11 @@ aria-label="Search Results Dropdown" class="c-gsearch__dropdown" > +