diff --git a/.github/workflows/matomo-tests.yml b/.github/workflows/matomo-tests.yml index 50ce7f0..5c62631 100644 --- a/.github/workflows/matomo-tests.yml +++ b/.github/workflows/matomo-tests.yml @@ -56,3 +56,25 @@ jobs: redis-service: true artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} upload-artifacts: ${{ matrix.php == '7.4' && matrix.target == 'maximum_supported_matomo' }} + artifacts-protected: true + github-token: ${{ secrets.TESTS_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} + UI: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + with: + lfs: true + persist-credentials: false + - name: running tests + uses: matomo-org/github-action-tests@main + with: + plugin-name: 'ApiReference' + matomo-test-branch: 'maximum_supported_matomo' + test-type: 'UI' + php-version: 'matomo5_max_php' + node-version: '16' + redis-service: true + artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} + upload-artifacts: true + artifacts-protected: true + github-token: ${{ secrets.TESTS_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 134b769..d6c13e5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ vendor/**/phpunit.xml* vendor/**/phpcs.xml* vendor/**/composer.json vendor/**/composer.lock +/tests/UI/processed-ui-screenshots/ /vue/dist/demo.html /vue/dist/*.common.js /vue/dist/*.map diff --git a/tests/Fixtures/SwaggerPageFixture.php b/tests/Fixtures/SwaggerPageFixture.php new file mode 100644 index 0000000..296044c --- /dev/null +++ b/tests/Fixtures/SwaggerPageFixture.php @@ -0,0 +1,90 @@ +setUpWebsite(); + $this->markApiReferenceAsInstalled(); + $this->writeOpenApiSpecFixtures(); + } + + public function tearDown(): void + { + $this->removeOpenApiSpecFixtures(); + } + + private function setUpWebsite(): void + { + if (!self::siteCreated($this->idSite)) { + $idSite = self::createWebsite($this->dateTime); + $this->assertSame($this->idSite, $idSite); + } + } + + private function writeOpenApiSpecFixtures(): void + { + $resolver = new PathResolver(); + $specDirectory = $resolver->getSpecDirectory(); + $specPath = $this->getBandwidthSpecPath(); + + if (!is_dir($specDirectory)) { + mkdir($specDirectory, 0777, true); + } + + if (is_file($specPath)) { + $this->hadOriginalSpecFixture = true; + $originalContents = file_get_contents($specPath); + $this->originalSpecFixtureContents = $originalContents === false ? null : $originalContents; + } + + copy($this->getBandwidthSpecFixturePath(), $specPath); + } + + private function markApiReferenceAsInstalled(): void + { + (new Updater())->markComponentSuccessfullyUpdated('ApiReference', '5.0.0'); + } + + private function removeOpenApiSpecFixtures(): void + { + $specPath = $this->getBandwidthSpecPath(); + + if ($this->hadOriginalSpecFixture) { + file_put_contents($specPath, $this->originalSpecFixtureContents ?? ''); + } elseif (is_file($specPath)) { + unlink($specPath); + } + } + + private function getBandwidthSpecPath(): string + { + return (new PathResolver())->getSpecFilePath('Bandwidth', ApiReference::DEFAULT_SPEC_VERSION); + } + + private function getBandwidthSpecFixturePath(): string + { + return __DIR__ . '/../Resources/SwaggerPage/Bandwidth_openapi_spec_v1.0.0.json'; + } +} diff --git a/tests/Resources/SwaggerPage/Bandwidth_openapi_spec_v1.0.0.json b/tests/Resources/SwaggerPage/Bandwidth_openapi_spec_v1.0.0.json new file mode 100644 index 0000000..1e440b1 --- /dev/null +++ b/tests/Resources/SwaggerPage/Bandwidth_openapi_spec_v1.0.0.json @@ -0,0 +1,991 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Matomo Reporting API for Bandwidth plugin", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://5x-dev.ddev.site/", + "description": "Current Matomo instance" + } + ], + "paths": { + "/index.php?module=API&method=Bandwidth.get": { + "get": { + "tags": [ + "Bandwidth" + ], + "description": "Returns aggregated bandwidth metrics for the requested site selection and archive period.", + "operationId": "Bandwidth.get", + "parameters": [ + { + "$ref": "#/components/parameters/formatOptional" + }, + { + "name": "idSite", + "in": "query", + "description": "Website ID(s) to query. - Single site ID (e.g. 1) - Multiple site IDs (e.g. [1, 4, 5]) - Comma-separated list (\"1,4,5\") or \"all\"", + "required": true, + "schema": { + "oneOf": [ + { + "type": "integer", + "example": 1 + }, + { + "type": "string", + "example": "1" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "example": 1 + } + ] + } + }, + { + "name": "period", + "in": "query", + "description": "The period to process, processes data for the period containing the specified date.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "day", + "week", + "month", + "year", + "range" + ], + "example": "day" + } + }, + { + "name": "date", + "in": "query", + "description": "The date or date range to process. 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX).", + "required": true, + "schema": { + "type": "string", + "example": "yesterday" + } + }, + { + "name": "segment", + "in": "query", + "description": "Custom segment to filter the report. Example: \"referrerName==example.com\" Supports AND (;) and OR (,) operators.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "columns", + "in": "query", + "description": "Metric columns to return. Accepts a comma-separated list, an array of metric names, or false to return all available bandwidth metrics.", + "required": false, + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": {} + } + ] + } + } + ], + "responses": { + "200": { + "description": "A table containing the requested bandwidth metric totals as integers.", + "content": { + "text/xml": { + "schema": { + "type": "object", + "xml": { + "name": "result" + } + }, + "example": { + "nb_total_overall_bandwidth": "0", + "nb_total_pageview_bandwidth": "0", + "nb_total_download_bandwidth": "0" + } + }, + "application/json": { + "schema": { + "properties": { + "nb_total_overall_bandwidth": { + "type": "integer" + }, + "nb_total_pageview_bandwidth": { + "type": "integer" + }, + "nb_total_download_bandwidth": { + "type": "integer" + } + }, + "type": "object" + }, + "example": { + "nb_total_overall_bandwidth": 0, + "nb_total_pageview_bandwidth": 0, + "nb_total_download_bandwidth": 0 + } + }, + "application/vnd.ms-excel": { + "example": "nb_total_overall_bandwidth\tnb_total_pageview_bandwidth\tnb_total_download_bandwidth\n0\t0\t0" + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + }, + "default": { + "$ref": "#/components/responses/DefaultError" + } + } + } + } + }, + "components": { + "schemas": { + "GenericSuccess": { + "description": "Generic Matomo success payload.", + "required": [ + "result", + "message" + ], + "properties": { + "result": { + "type": "string", + "example": "success" + }, + "message": { + "type": "string", + "example": "ok" + }, + "code": { + "type": "integer", + "example": "200" + } + }, + "type": "object", + "example": { + "result": "success", + "message": "ok" + }, + "additionalProperties": true + }, + "GenericSuccessXml": { + "description": "Generic Matomo success payload in XML.", + "required": [ + "success" + ], + "properties": { + "success": { + "properties": { + "message": { + "type": "string", + "xml": { + "attribute": true + }, + "example": "ok" + } + }, + "type": "object", + "xml": { + "name": "success" + } + } + }, + "type": "object", + "xml": { + "name": "result" + }, + "example": { + "success": { + "message": "ok" + } + }, + "additionalProperties": true + }, + "Error": { + "description": "Generic Matomo error payload.", + "required": [ + "result", + "message" + ], + "properties": { + "result": { + "type": "string", + "example": "error" + }, + "message": { + "type": "string", + "example": "There was an error" + }, + "code": { + "type": "integer" + } + }, + "type": "object", + "additionalProperties": true + }, + "ErrorXml": { + "description": "Generic Matomo error payload in XML.", + "properties": { + "error": { + "properties": { + "message": { + "type": "string", + "xml": { + "attribute": true + }, + "example": "There was an error" + } + }, + "type": "object", + "xml": { + "name": "error" + } + } + }, + "type": "object", + "xml": { + "name": "result" + } + } + }, + "responses": { + "BadRequest": { + "description": "Bad request (validation or missing parameters).", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Error: There was an error." + }, + "text/html": { + "schema": { + "type": "string" + }, + "example": "There was an error." + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ErrorXml" + } + } + } + }, + "Unauthorized": { + "description": "Authentication failed or missing token.", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Error: You must be logged in to access this functionality." + }, + "text/html": { + "schema": { + "type": "string" + }, + "example": "You must be logged in to access this functionality." + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ErrorXml" + } + } + } + }, + "Forbidden": { + "description": "Authenticated but not allowed to access the resource.", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Error: Not authorised." + }, + "text/html": { + "schema": { + "type": "string" + }, + "example": "Not authorised." + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ErrorXml" + } + } + } + }, + "NotFound": { + "description": "Resource not found.", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Error: The method is not available." + }, + "text/html": { + "schema": { + "type": "string" + }, + "example": "The method is not available." + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ErrorXml" + } + } + } + }, + "ServerError": { + "description": "Unexpected server error.", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Error: There was an error." + }, + "text/html": { + "schema": { + "type": "string" + }, + "example": "There was an error." + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ErrorXml" + } + } + } + }, + "DefaultError": { + "description": "Default error response (any non-2xx).", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Error: There was an error." + }, + "text/html": { + "schema": { + "type": "string" + }, + "example": "There was an error." + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ErrorXml" + } + } + } + }, + "GenericSuccessNoBody": { + "description": "Generic 200 response with no body" + }, + "GenericSuccess": { + "description": "Generic 200 response", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Success:ok" + }, + "text/html": { + "schema": { + "type": "string" + }, + "example": "" + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericSuccess" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/GenericSuccessXml" + } + } + } + }, + "GenericString": { + "description": "Generic 200 response with only a string body", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Result: success" + }, + "text/html": { + "schema": { + "type": "string" + }, + "example": "success" + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "GenericBoolean": { + "description": "Generic 200 response with only true or false as the body", + "content": { + "text/plain": { + "schema": { + "type": "boolean" + } + }, + "text/html": { + "schema": { + "type": "boolean" + } + }, + "application/json": { + "schema": { + "type": "boolean" + } + }, + "application/xml": { + "schema": { + "type": "boolean" + } + } + } + }, + "GenericInteger": { + "description": "Generic 200 response with only an integer as the body", + "content": { + "text/plain": { + "schema": { + "type": "integer" + } + }, + "text/html": { + "schema": { + "type": "integer" + } + }, + "application/json": { + "schema": { + "type": "integer" + } + }, + "application/xml": { + "schema": { + "type": "integer" + } + } + } + }, + "GenericArray": { + "description": "Generic 200 response with array body", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "array", + "items": {} + } + }, + "application/xml": { + "schema": { + "type": "array", + "items": {} + } + } + } + } + }, + "parameters": { + "moduleRequired": { + "name": "module", + "in": "query", + "description": "Always `API` for Reporting API requests.", + "required": true, + "schema": { + "type": "string", + "default": "API" + } + }, + "methodRequired": { + "name": "method", + "in": "query", + "description": "API method, e.g. `VisitsSummary.get` or `CustomAlerts.getAlert`.", + "required": true, + "schema": { + "type": "string", + "example": "CustomAlerts.getAlert" + } + }, + "formatRequired": { + "name": "format", + "in": "query", + "description": "Response format such as `xml` or `json`. Use `original` to get the original PHP data structure.", + "required": true, + "schema": { + "type": "string", + "default": "xml", + "enum": [ + "xml", + "json", + "csv", + "tsv", + "html", + "rss", + "original" + ] + } + }, + "formatOptional": { + "name": "format", + "in": "query", + "description": "Response format. Defaults to `xml`. Use `original` to get the original PHP data structure.", + "required": false, + "schema": { + "type": "string", + "default": "xml", + "enum": [ + "xml", + "json", + "csv", + "tsv", + "html", + "rss", + "original" + ] + } + }, + "idSiteRequired": { + "name": "idSite", + "in": "query", + "description": "Matomo site ID.", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + }, + "idSiteOptional": { + "name": "idSite", + "in": "query", + "description": "Matomo site ID.", + "required": false, + "schema": { + "type": "integer", + "example": 1 + } + }, + "periodRequired": { + "name": "period", + "in": "query", + "description": "Reporting period.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "day", + "week", + "month", + "year", + "range" + ], + "example": "day" + } + }, + "periodOptional": { + "name": "period", + "in": "query", + "description": "Reporting period.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "day", + "week", + "month", + "year", + "range" + ], + "example": "day" + } + }, + "dateRequired": { + "name": "date", + "in": "query", + "description": "Date or range (e.g. `2025-08-01`, `yesterday`, `last30`, or `2025-08-01,2025-08-11`).", + "required": true, + "schema": { + "type": "string", + "example": "today" + } + }, + "dateOptional": { + "name": "date", + "in": "query", + "description": "Date or range (e.g. `2025-08-01`, `yesterday`, `last30`, or `2025-08-01,2025-08-11`).", + "required": false, + "schema": { + "type": "string", + "example": "today" + } + }, + "segmentRequired": { + "name": "segment", + "in": "query", + "description": "Segment expression; see `API.getSegmentDimensionMetadata`.", + "required": true, + "schema": { + "type": "string" + } + }, + "segmentOptional": { + "name": "segment", + "in": "query", + "description": "Segment expression; see `API.getSegmentDimensionMetadata`.", + "required": false, + "schema": { + "type": "string" + } + }, + "expandedOptional": { + "name": "expanded", + "in": "query", + "description": "If true, loads all subtables.", + "required": false, + "schema": { + "type": "integer", + "default": 0, + "enum": [ + 0, + 1 + ], + "example": 0 + } + }, + "idSubtableOptional": { + "name": "idSubtable", + "in": "query", + "description": "An in-database subtable ID.", + "required": false, + "schema": { + "type": "integer" + } + }, + "idSubtableRequired": { + "name": "idSubtable", + "in": "query", + "description": "An in-database subtable ID.", + "required": true, + "schema": { + "type": "integer" + } + }, + "flatOptional": { + "name": "flat", + "in": "query", + "description": "Flatten subtables into the parent table.", + "required": false, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "example": 0 + } + }, + "filter_patternOptional": { + "name": "filter_pattern", + "in": "query", + "description": "Regex to keep matching rows.", + "required": false, + "schema": { + "type": "string" + } + }, + "filter_columnOptional": { + "name": "filter_column", + "in": "query", + "description": "Column to apply the regex to (e.g., `label`).", + "required": false, + "schema": { + "type": "string", + "example": "label" + } + }, + "filter_pattern_recursiveOptional": { + "name": "filter_pattern_recursive", + "in": "query", + "description": "Recursive regex filter.", + "required": false, + "schema": { + "type": "string" + } + }, + "filter_column_recursiveOptional": { + "name": "filter_column_recursive", + "in": "query", + "description": "Column for the recursive regex filter.", + "required": false, + "schema": { + "type": "string" + } + }, + "filter_excludelowpopOptional": { + "name": "filter_excludelowpop", + "in": "query", + "description": "Column to threshold and exclude low values.", + "required": false, + "schema": { + "type": "string" + } + }, + "filter_excludelowpop_valueOptional": { + "name": "filter_excludelowpop_value", + "in": "query", + "description": "Minimum value threshold for `filter_excludelowpop`.", + "required": false, + "schema": { + "type": "number", + "example": 0 + } + }, + "filter_sort_columnOptional": { + "name": "filter_sort_column", + "in": "query", + "description": "Column to sort by.", + "required": false, + "schema": { + "type": "string" + } + }, + "filter_sort_orderOptional": { + "name": "filter_sort_order", + "in": "query", + "description": "Sort direction.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "example": "desc" + } + }, + "filter_truncateOptional": { + "name": "filter_truncate", + "in": "query", + "description": "Row index after which rows are removed.", + "required": false, + "schema": { + "type": "integer" + } + }, + "filter_limitOptional": { + "name": "filter_limit", + "in": "query", + "description": "Maximum number of rows to return.", + "required": false, + "schema": { + "type": "integer" + } + }, + "filter_offsetOptional": { + "name": "filter_offset", + "in": "query", + "description": "Row offset.", + "required": false, + "schema": { + "type": "integer" + } + }, + "keep_summary_rowOptional": { + "name": "keep_summary_row", + "in": "query", + "description": "Keep the summary row.", + "required": false, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "example": 1 + } + }, + "disable_generic_filtersOptional": { + "name": "disable_generic_filters", + "in": "query", + "description": "Disable generic filters (those above).", + "required": false, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "example": 0 + } + }, + "disable_queued_filtersOptional": { + "name": "disable_queued_filters", + "in": "query", + "description": "Skip queued filters.", + "required": false, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "example": 0 + } + }, + "hideColumnsOptional": { + "name": "hideColumns", + "in": "query", + "description": "Comma-separated list of columns to hide.", + "required": false, + "schema": { + "type": "string" + } + }, + "showColumnsOptional": { + "name": "showColumns", + "in": "query", + "description": "Comma-separated list of columns to include.", + "required": false, + "schema": { + "type": "string" + } + }, + "labelOptional": { + "name": "label", + "in": "query", + "description": "Keep only rows with these label(s). Supports path via '>' and arrays.", + "required": false, + "schema": { + "type": "string" + } + }, + "idGoalRequired": { + "name": "idGoal", + "in": "query", + "description": "The ID of a configured goal.", + "required": true, + "schema": { + "type": "integer" + } + }, + "idGoalOptional": { + "name": "idGoal", + "in": "query", + "description": "The ID of a configured goal.", + "required": false, + "schema": { + "type": "integer" + } + } + }, + "securitySchemes": { + "MatomoToken": { + "type": "http", + "description": "Matomo API token passed in the Authorization header as a bearer token.", + "scheme": "bearer" + } + } + }, + "security": [ + { + "MatomoToken": [] + } + ], + "tags": [ + { + "name": "Bandwidth", + "description": "Exposes reporting API endpoints for aggregated bandwidth metrics." + } + ], + "externalDocs": { + "description": "Matomo Reporting API developer page", + "url": "https://developer.matomo.org/api-reference/reporting-api/" + } +} \ No newline at end of file diff --git a/tests/UI/ApiReference_spec.js b/tests/UI/ApiReference_spec.js new file mode 100644 index 0000000..d2499a7 --- /dev/null +++ b/tests/UI/ApiReference_spec.js @@ -0,0 +1,121 @@ +/*! + * Matomo - free/libre analytics platform + * + * ApiReference screenshot tests. + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe('ApiReference', function () { + this.fixture = 'Piwik\\Plugins\\ApiReference\\tests\\Fixtures\\SwaggerPageFixture'; + + const pageUrl = '?module=ApiReference&action=swagger&idSite=1&period=day&date=2010-01-03'; + const targetPlugin = 'Bandwidth'; + const searchInputSelector = '.searchInput'; + + before(function () { + testEnvironment.pluginsToLoad = ['ApiReference', 'Bandwidth']; + testEnvironment.testUseMockAuth = 1; + testEnvironment.overrideConfig('General', 'enable_auto_update', 0); + testEnvironment.save(); + }); + + after(function () { + delete testEnvironment.pluginsToLoad; + delete testEnvironment.configOverride.General; + testEnvironment.testUseMockAuth = 1; + testEnvironment.save(); + }); + + async function moveMouseAway() { + await page.mouse.move(-10, -10); + } + + async function waitForUiToSettle(delay = 150) { + await page.waitForNetworkIdle(); + await page.waitForTimeout(delay); + } + + async function loadSwaggerPage() { + await page.goto(pageUrl); + await page.waitForSelector(searchInputSelector); + await page.waitForSelector('.pluginList .pluginCard'); + await waitForUiToSettle(); + await moveMouseAway(); + } + + async function searchFor(term) { + await page.evaluate((selector, value) => { + const input = document.querySelector(selector); + + if (!input) { + throw new Error(`Unable to find ${selector}`); + } + + input.value = value; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + }, searchInputSelector, term); + await page.waitForTimeout(150); + await moveMouseAway(); + } + + async function expandPlugin(pluginName) { + await page.evaluate((targetName) => { + const pluginButtons = Array.from(document.querySelectorAll('.pluginToggle')); + const pluginButton = pluginButtons.find((button) => { + const pluginNameElement = button.querySelector('.pluginName'); + return pluginNameElement && pluginNameElement.textContent.trim() === targetName; + }); + + if (!pluginButton) { + throw new Error(`Unable to find plugin card for ${targetName}`); + } + + pluginButton.click(); + }, pluginName); + + await page.waitForSelector('.pluginCard--expanded'); + await page.waitForSelector('.pluginCard--expanded .swaggerMount--ready'); + await page.waitForSelector('.pluginCard--expanded .swagger-ui .opblock-tag-section'); + await waitForUiToSettle(250); + await moveMouseAway(); + } + + async function waitForSingleVisiblePlugin(pluginName) { + await page.waitForFunction((expectedPluginName) => { + const visiblePluginNames = Array.from(document.querySelectorAll('.pluginCard')) + .filter((card) => card.offsetParent !== null) + .map((card) => card.querySelector('.pluginName')?.textContent?.trim()) + .filter(Boolean); + + return visiblePluginNames.length === 1 && visiblePluginNames[0] === expectedPluginName; + }, {}, pluginName); + } + + it('should show the filtered plugin search result', async function () { + await loadSwaggerPage(); + await searchFor('bandwidth'); + await waitForSingleVisiblePlugin(targetPlugin); + + expect(await page.screenshotSelector('.searchBar,.pluginList')).to.matchImage('filtered_plugin'); + }); + + it('should show the empty plugin search result', async function () { + await loadSwaggerPage(); + await searchFor('no-plugin-match'); + await page.waitForSelector('.emptyText'); + + expect(await page.screenshotSelector('.searchBar,.emptyText')).to.matchImage('empty_search'); + }); + + it('should show the expanded Swagger UI for a plugin', async function () { + await loadSwaggerPage(); + await searchFor('bandwidth'); + await waitForSingleVisiblePlugin(targetPlugin); + await expandPlugin(targetPlugin); + + expect(await page.screenshotSelector('.searchBar,.pluginList')).to.matchImage('expanded_plugin'); + }); +}); diff --git a/tests/UI/expected-ui-screenshots/ApiReference_empty_search.png b/tests/UI/expected-ui-screenshots/ApiReference_empty_search.png new file mode 100644 index 0000000..6b31d98 Binary files /dev/null and b/tests/UI/expected-ui-screenshots/ApiReference_empty_search.png differ diff --git a/tests/UI/expected-ui-screenshots/ApiReference_expanded_plugin.png b/tests/UI/expected-ui-screenshots/ApiReference_expanded_plugin.png new file mode 100644 index 0000000..2060952 Binary files /dev/null and b/tests/UI/expected-ui-screenshots/ApiReference_expanded_plugin.png differ diff --git a/tests/UI/expected-ui-screenshots/ApiReference_filtered_plugin.png b/tests/UI/expected-ui-screenshots/ApiReference_filtered_plugin.png new file mode 100644 index 0000000..4a158aa Binary files /dev/null and b/tests/UI/expected-ui-screenshots/ApiReference_filtered_plugin.png differ diff --git a/vue/dist/ApiReference.css b/vue/dist/ApiReference.css index ad3d531..1561b16 100644 --- a/vue/dist/ApiReference.css +++ b/vue/dist/ApiReference.css @@ -1 +1 @@ -.swaggerLoader[data-v-5be4746a]{max-height:100px;display:flex;align-items:center;justify-content:center}.swaggerMount[data-v-5be4746a]{min-height:180px;visibility:hidden}.swaggerMount--ready[data-v-5be4746a]{visibility:visible}.swaggerMount[data-v-5be4746a] .swagger-ui{border:0;border-radius:0;color:var(--theme-color-text,#3b4151);font-size:14px;line-height:1.5;padding-top:0}.page[data-v-86969ddc]{color:var(--theme-color-text,#3b4151)}.searchBar[data-v-86969ddc]{position:relative;margin-bottom:1.5rem;width:300px}.searchIcon[data-v-86969ddc]{position:absolute;top:13px;left:12px;color:var(--theme-color-text-lighter,#98a2b3);font-size:14px;pointer-events:none}.searchInput[data-v-86969ddc]{width:100%;height:38px;padding:10px 12px 10px 38px;background:var(--theme-color-background-contrast,#fff);border:1px solid var(--theme-color-border,#d0d5dd);border-radius:8px;color:var(--theme-color-text,#3b4151);font-size:14px;box-shadow:none}.searchInput[data-v-86969ddc]:focus-visible{border:1px solid var(--theme-color-focus-ring,#5b8def);outline:1px solid var(--theme-color-focus-ring,#5b8def)}.searchInput[data-v-86969ddc]::-moz-placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.searchInput[data-v-86969ddc]::-ms-input-placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.searchInput[data-v-86969ddc]::placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.emptyText[data-v-86969ddc]{margin-bottom:0;color:var(--theme-color-text-light,#646464)}.old-api-docs-paragraph[data-v-86969ddc]{font-size:12px!important;font-style:italic}.pluginCard[data-v-86969ddc]{background:var(--theme-color-background-contrast,#fff);border:1px solid var(--theme-color-border,#d9e2ec);border-radius:4px;box-shadow:none;overflow:hidden;transition:border-color .18s ease}.pluginCard--expanded[data-v-86969ddc]{border-color:var(--theme-color-border,#cfd8e3);transform-origin:top center}.pluginToggle[data-v-86969ddc]{width:100%;padding:16px 20px;border:0;outline:none;background:var(--theme-color-background-contrast,#fff);display:flex;align-items:center;color:inherit;cursor:pointer;font:inherit;text-align:left}.pluginToggle[data-v-86969ddc]:focus-visible{box-shadow:inset 0 0 0 2px var(--theme-color-focus-ring,#cfd8e3)}.pluginHeader[data-v-86969ddc]{display:flex;align-items:center;gap:12px}.pluginChevron[data-v-86969ddc]{flex:0 0 12px;color:var(--theme-color-text-light,#5b6b7c);font-size:12px;display:inline-flex;align-items:center;justify-content:center}.pluginName[data-v-86969ddc]{font-size:15px;font-weight:500}.pluginBody[data-v-86969ddc]{position:relative;padding:0}.pluginBody[data-v-86969ddc]:before{content:"";position:absolute;top:0;left:20px;right:20px;border-top:1px solid var(--theme-color-border,#e6edf5)} \ No newline at end of file +.swaggerLoader[data-v-590bd90f]{max-height:100px;display:flex;align-items:center;justify-content:center}.swaggerMount[data-v-590bd90f]{min-height:180px;visibility:hidden}.swaggerMount--ready[data-v-590bd90f]{visibility:visible}.swaggerMount[data-v-590bd90f] .swagger-ui{border:0;border-radius:0;color:var(--theme-color-text,#3b4151);font-size:14px;line-height:1.5;padding-top:0}.page[data-v-86969ddc]{color:var(--theme-color-text,#3b4151)}.searchBar[data-v-86969ddc]{position:relative;margin-bottom:1.5rem;width:300px}.searchIcon[data-v-86969ddc]{position:absolute;top:13px;left:12px;color:var(--theme-color-text-lighter,#98a2b3);font-size:14px;pointer-events:none}.searchInput[data-v-86969ddc]{width:100%;height:38px;padding:10px 12px 10px 38px;background:var(--theme-color-background-contrast,#fff);border:1px solid var(--theme-color-border,#d0d5dd);border-radius:8px;color:var(--theme-color-text,#3b4151);font-size:14px;box-shadow:none}.searchInput[data-v-86969ddc]:focus-visible{border:1px solid var(--theme-color-focus-ring,#5b8def);outline:1px solid var(--theme-color-focus-ring,#5b8def)}.searchInput[data-v-86969ddc]::-moz-placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.searchInput[data-v-86969ddc]::-ms-input-placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.searchInput[data-v-86969ddc]::placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.emptyText[data-v-86969ddc]{margin-bottom:0;color:var(--theme-color-text-light,#646464)}.old-api-docs-paragraph[data-v-86969ddc]{font-size:12px!important;font-style:italic}.pluginCard[data-v-86969ddc]{background:var(--theme-color-background-contrast,#fff);border:1px solid var(--theme-color-border,#d9e2ec);border-radius:4px;box-shadow:none;overflow:hidden;transition:border-color .18s ease}.pluginCard--expanded[data-v-86969ddc]{border-color:var(--theme-color-border,#cfd8e3);transform-origin:top center}.pluginToggle[data-v-86969ddc]{width:100%;padding:16px 20px;border:0;outline:none;background:var(--theme-color-background-contrast,#fff);display:flex;align-items:center;color:inherit;cursor:pointer;font:inherit;text-align:left}.pluginToggle[data-v-86969ddc]:focus-visible{box-shadow:inset 0 0 0 2px var(--theme-color-focus-ring,#cfd8e3)}.pluginHeader[data-v-86969ddc]{display:flex;align-items:center;gap:12px}.pluginChevron[data-v-86969ddc]{flex:0 0 12px;color:var(--theme-color-text-light,#5b6b7c);font-size:12px;display:inline-flex;align-items:center;justify-content:center}.pluginName[data-v-86969ddc]{font-size:15px;font-weight:500}.pluginBody[data-v-86969ddc]{position:relative;padding:0}.pluginBody[data-v-86969ddc]:before{content:"";position:absolute;top:0;left:20px;right:20px;border-top:1px solid var(--theme-color-border,#e6edf5)} \ No newline at end of file diff --git a/vue/dist/ApiReference.umd.js b/vue/dist/ApiReference.umd.js index 5855bde..d336e6b 100644 --- a/vue/dist/ApiReference.umd.js +++ b/vue/dist/ApiReference.umd.js @@ -96,14 +96,10 @@ return /******/ (function(modules) { // webpackBootstrap /************************************************************************/ /******/ ({ -/***/ "136e": -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_5be4746a_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("ad84"); -/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_5be4746a_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_5be4746a_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__); -/* unused harmony reexport * */ +/***/ "1988": +/***/ (function(module, exports, __webpack_require__) { +// extracted by mini-css-extract-plugin /***/ }), @@ -139,10 +135,14 @@ module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; /***/ }), -/***/ "ad84": -/***/ (function(module, exports, __webpack_require__) { +/***/ "dc2f": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_590bd90f_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("1988"); +/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_590bd90f_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_590bd90f_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__); +/* unused harmony reexport * */ -// extracted by mini-css-extract-plugin /***/ }), @@ -298,19 +298,19 @@ function render(_ctx, _cache, $props, $setup, $data, $options) { // EXTERNAL MODULE: external "CoreHome" var external_CoreHome_ = __webpack_require__("19dc"); -// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/ApiReference/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=template&id=5be4746a&scoped=true +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/ApiReference/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=template&id=590bd90f&scoped=true -const SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_withScopeId = n => (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["pushScopeId"])("data-v-5be4746a"), n = n(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["popScopeId"])(), n); -const SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_hoisted_1 = { +const SwaggerUiPanelvue_type_template_id_590bd90f_scoped_true_withScopeId = n => (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["pushScopeId"])("data-v-590bd90f"), n = n(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["popScopeId"])(), n); +const SwaggerUiPanelvue_type_template_id_590bd90f_scoped_true_hoisted_1 = { key: 0, class: "swaggerLoader" }; -const SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_hoisted_2 = ["innerHTML"]; -const SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_hoisted_3 = ["id"]; -function SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_render(_ctx, _cache, $props, $setup, $data, $options) { +const SwaggerUiPanelvue_type_template_id_590bd90f_scoped_true_hoisted_2 = ["innerHTML"]; +const SwaggerUiPanelvue_type_template_id_590bd90f_scoped_true_hoisted_3 = ["id"]; +function SwaggerUiPanelvue_type_template_id_590bd90f_scoped_true_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_ActivityIndicator = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ActivityIndicator"); const _component_Alert = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Alert"); - return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, [_ctx.isLoading && !_ctx.spec && !_ctx.displayError ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ActivityIndicator, { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, [_ctx.isLoading && !_ctx.spec && !_ctx.displayError ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", SwaggerUiPanelvue_type_template_id_590bd90f_scoped_true_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ActivityIndicator, { loading: true })])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.displayError ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_Alert, { key: 1, @@ -324,16 +324,16 @@ function SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_render(_ctx, _c }, { default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { innerHTML: _ctx.$sanitize(_ctx.missingSpecLearnMore) - }, null, 8, SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_hoisted_2)]), + }, null, 8, SwaggerUiPanelvue_type_template_id_590bd90f_scoped_true_hoisted_2)]), _: 1 })) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", { id: _ctx.swaggerContainerId, class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(['swaggerMount', { 'swaggerMount--ready': _ctx.isReady }]) - }, null, 10, SwaggerUiPanelvue_type_template_id_5be4746a_scoped_true_hoisted_3)], 64); + }, null, 10, SwaggerUiPanelvue_type_template_id_590bd90f_scoped_true_hoisted_3)], 64); } -// CONCATENATED MODULE: ./plugins/ApiReference/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=template&id=5be4746a&scoped=true +// CONCATENATED MODULE: ./plugins/ApiReference/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=template&id=590bd90f&scoped=true // CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/ApiReference/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=script&lang=ts @@ -410,6 +410,13 @@ const copySuccessIconMarkup = ' Object.prototype.hasOwnProperty.call(object, property); + }, getSwaggerRoot() { return document.getElementById(this.swaggerContainerId); }, @@ -579,6 +586,7 @@ const copySuccessIconMarkup = '