├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── documentation ├── ci-mode.md └── images │ ├── CI-Figure1.png │ ├── Figure3.gif │ ├── Figure4.png │ ├── automated-testing-ado-option.png │ ├── automated-testing-create-pipeline.png │ ├── automated-testing-create-variable-group.png │ ├── automated-testing-library.png │ ├── automated-testing-navigate-pipeline.png │ ├── automated-testing-permit-again.png │ ├── automated-testing-permit.png │ ├── automated-testing-save-and-run.png │ ├── automated-testing-save-variable-group.png │ ├── automated-testing-select-repo.png │ ├── automated-testing-variable-group.png │ ├── configure-your-pipeline.png │ ├── failed-tests.png │ ├── rdl-error.png │ ├── rdl-multi.png │ ├── rdl-test-file.png │ ├── review-your-pipeline-YAML.png │ ├── run-playwright-tests.png │ ├── select-an-existing-YAML-file.png │ ├── test-cases.jpg │ └── ui-test-generation.png ├── generate-test-cases.ps1 ├── global-setup.ts ├── helper-functions ├── file-reader.ts ├── logging.ts └── token-helpers.ts ├── package-lock.json ├── package.json ├── pipeline-scripts ├── playwright-automation.yml ├── playwright-docker-template.yml ├── playwright-service-template.yml └── playwright-version1-template.yml ├── playwright.config.ts ├── playwright.config.v1.ts ├── playwright.service.config.ts ├── template.env ├── test-cases └── placeholder.txt ├── test-generation ├── Execute-XMLAQuery.ps1 ├── app.js ├── package-lock.json ├── package.json └── views │ └── index.html └── tests ├── paginated.spec.ts └── pbi.spec.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | localsettings.json 7 | /test-results/ 8 | /playwright-report/ 9 | /blob-report/ 10 | /playwright/.cache/ 11 | test-cases/*.csv 12 | test-cases/*.json 13 | .env 14 | /release/ 15 | 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "powerbi.workspaceFilter": "query" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 John Kerski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pbi-dataops-visual-error-testing 2 | 3 | Templates for testing Power BI reports for broken visuals using PowerShell and Microsoft Playwright. This project provides a robust framework to automate testing, ensuring that Power BI reports render correctly without broken visuals that could impact user experience. 4 | 5 | ## Table of Contents 6 | 7 | - [pbi-dataops-visual-error-testing](#pbi-dataops-visual-error-testing) 8 | - [Table of Contents](#table-of-contents) 9 | - [Prerequisites](#prerequisites) 10 | - [Setup](#setup) 11 | - [1. Copy the Project](#1-copy-the-project) 12 | - [2. Anatomy of the Project](#2-anatomy-of-the-project) 13 | - [3. Update Environment Variables](#3-update-environment-variables) 14 | - [4. Add Test Cases](#4-add-test-cases) 15 | - [PowerShell Approach](#powershell-approach) 16 | - [User Interface Approach](#user-interface-approach) 17 | - [Paginated Report Support](#paginated-report-support) 18 | - [Multiple Value Parameters](#multiple-value-parameters) 19 | - [5. Run the Tests Locally](#5-run-the-tests-locally) 20 | - [Reading the Results](#reading-the-results) 21 | - [Broken Visuals](#broken-visuals) 22 | - [Continuous Integration](#continuous-integration) 23 | - [Limitations](#limitations) 24 | 25 | ## Prerequisites 26 | 27 | Before setting up the project, ensure that the following prerequisites are met: 28 | 29 | 1. **Install VSCode** 30 | Download and install Visual Studio Code from [here](https://code.visualstudio.com/). 31 | 32 | 2. **Install Node** 33 | Install Node.js and npm from [here](https://nodejs.org/). This is required to run Playwright and other JavaScript-based tools. 34 | 35 | 3. **Power BI Licensing** 36 | Ensure your Power BI workspace is within at least a Premium Per User Capacity. This is necessary for programmatic access and automation capabilities. 37 | 38 | 4. **Create App Service Principal** 39 | A service principal is required to authenticate and perform actions in the Power BI service. 40 | - Go to the Azure portal to register a new application and create a service principal. 41 | - Provide the following API permissions with admin consent granted: 42 | - `App.Read.All` 43 | - `Dataset.Read.All` 44 | - `SemanticModel.Read.All` 45 | - `Report.Read.All` 46 | - `Workspace.Read.All` 47 | - Create a secret for the service principal. 48 | - Note down the **Client ID**, **Tenant ID**, and **Client Secret** as these will be needed for configuration later. 49 | 50 | 5. **Enable XMLA Endpoint Access** 51 | Set up the XMLA endpoint for your workspace. Ensure that the workspaces being tested are set to the correct level and that necessary users or the service principal have **Member** rights to the workspaces. 52 | 53 | 6. **Setup Playwright** 54 | Install and set up Playwright for testing. Follow the setup instructions provided in the [Playwright documentation](https://playwright.dev/docs/getting-started-vscode). 55 | 56 | ## Setup 57 | 58 | ### 1. Copy the Project 59 | 60 | 1. **Download from Releases** 61 | Navigate to the Releases section of this GitHub repository and download the latest release. 62 | 63 | 2. **Unzip the File** 64 | Extract the contents of the downloaded release. 65 | 66 | 3. **Open in Visual Studio Code** 67 | Open the extracted project folder in Visual Studio Code. 68 | 69 | ### 2. Anatomy of the Project 70 | 71 | The project consists of several key files and folders: 72 | 73 | 1. **template.env** 74 | A template file where environment variables are stored. This includes the client secret for the service principal. 75 | 76 | 2. **test** 77 | Contains the `pbi.spec.ts` file, which is the core code that executes the tests. 78 | 79 | 3. **helper-functions** 80 | This folder includes a set of reusable functions and interfaces that support `pbi.spec.ts`. 81 | 82 | 4. **global-setup.ts** 83 | Runs before any test and is used for global test setup. Currently, it logs some results to the console. 84 | 85 | 5. **test-cases** 86 | This folder contains CSV files that define specific test cases. Refer to the [CSV Section](https://github.com/kerski/get-powerbireportpagesfortesting) for more details. 87 | 88 | 6. **playwright.config.* files** 89 | Configuration files for Playwright that define how tests are executed. The default configuration file is `playwright.config.ts`. 90 | 91 | 7. **pipeline-scripts** 92 | Contains scripts that define how Playwright tests are executed in Azure Pipelines. 93 | 94 | ### 3. Update Environment Variables 95 | 96 | 1. **Rename `template.env` to `.env`** 97 | This `.env` file will be used to store environment-specific configurations. 98 | 99 | 2. **Set the following variables in the `.env` file:** 100 | - `CLIENT_ID=""` 101 | Set this to the Client ID obtained from the prerequisites. 102 | - `CLIENT_SECRET=""` 103 | Set this to the Client Secret created in the prerequisites. 104 | - `TENANT_ID=""` 105 | Set this to the Tenant ID obtained from the prerequisites. 106 | - `ENVIRONMENT=""` 107 | Set this to identify the tenant type (public or sovereign) where your Power BI service is located. Valid values are: Public, Germany, China, USGov, USGovHigh, or USGovDoD. 108 | - `EFFECTIVE_USERNAME=""` 109 | Set this to a User Principal Name you want to use for RLS testing. This account is used for impersonation when rendering RLS-based reports. 110 | 111 | 3. **Save the `.env` file** after updating all the necessary environment variables. 112 | 113 | ### 4. Add Test Cases 114 | 115 | For generating test cases there are two approaches. The first approach is the PowerShell approach where you generate the test cases based on a PowerShell module. This approach is helpful you understand PowerShell and if you are looking to generate test cases automatically in build pipelines. The second approach is the User Interface (UI) approach 116 | 117 | #### PowerShell Approach 118 | 119 | 1. **Open Terminal in Visual Studio Code** 120 | In Visual Studio Code, open the terminal by selecting **Terminal > New Terminal**. 121 | 122 | 2. **Run the generate-test-cases script** Execute the following command: 123 | ```bash 124 | \.generate-test-cases.ps1 125 | 3. **Respond to the prompts with which include**: 126 | - client_id - The id created by the service principal 127 | - client_secret - The secret by the service principal 128 | - tenant_id - The Tenant ID for the service principal 129 | - dataset_id - The Semantic Model/Dataset GUID to be tested 130 | - workspace_id - The Workspace GUID 131 | - role_user_name - Your email address/UPN 132 | 4. This will generate a test-case.csv file in the test-cases folder. 133 | ![Test Cases Example](./documentation/images/test-cases.jpg) 134 | 135 | #### User Interface Approach 136 | 137 | 1. **Open Terminal in Visual Studio Code** 138 | In Visual Studio Code, open the terminal by selecting **Terminal > New Terminal**. 139 | 2. **Navigate to test-generation folder** 140 | Execute the following command: 141 | ```bash 142 | cd test-generation 143 | 3. **Install application** 144 | Execute the following command: 145 | ```bash 146 | npm install 147 | 4. **Start application** 148 | Execute the following command: 149 | ```bash 150 | node app.js 151 | 5. **Open Browser** 152 | Open your browser of choice and enter "http://localhost:3000" in the address bar. 153 | 154 | ![UI Test Generation](./documentation/images/ui-test-generation.png) 155 | 156 | 6. **Generate Test** 157 | Follow the instructions on the page to generate tests for a specific report. 158 | 159 | #### Paginated Report Support 160 | 161 | Test generation now supports Paginated Reports that use Semantic Models for sources and will test for error modals that appear on the screen. 162 | 163 | ![RDL Error Example](./documentation/images/rdl-error.png) 164 | 165 | When a test is generated for a Paginated Report it will create a JSON file that produces an array of test cases. By default, a test case will be created that tries loading the paginated report and checks for errors. 166 | 167 | Each test has the following properties: 168 | 169 | - test_case: A unique identifier for the test case. 170 | - workspace_id: The GUID of the Power BI workspace where the report resides. 171 | - report_id: The GUID of the Power BI report being tested. 172 | - report_name: The name of the report being tested. 173 | - dataset_ids: An array of semantic models associated with the report. Each semantic model contains: 174 | - id: The GUID of the semantic model. 175 | - xmlaPermissions: The permissions level for the semantic model. 176 | - wait_seconds: The number of seconds to wait before performing the next action in the test (e.g., 20). 177 | 178 | If the paginated reports have parameters, it will create a template set of parameters within the JSON property `report_parameters`. You will need to replace the null value with an actual value so the test will pass those parameters to the report and render in the test. 179 | 180 | ![RDL Test File Example](./documentation/images/rdl-test-file.png) 181 | 182 | ##### Multiple Value Parameters 183 | 184 | If you have a parameter that requires multiple values, you can repeat the name, value pair within the report_parameters properties to pass multiple parameters into the paginated report during the test. 185 | 186 | ![Multivalue](./documentation/images/rdl-multi.png) 187 | 188 | ### 5. Run the Tests Locally 189 | 190 | 1. **Open Terminal in Visual Studio Code** 191 | In Visual Studio Code, open the terminal by selecting **Terminal > New Terminal**. 192 | 193 | 2. **npm install** Execute the following command: npm install. 194 | ```bash 195 | npm install 196 | 3. **npx playwright install** Execute the following command: 197 | ```bash 198 | npx playwright install 199 | 4. **Run Playwright Tests** 200 | Execute the following command to run Playwright tests locally: 201 | ```bash 202 | npx playwright test --workers=2 203 | 5. The tests will run in the following sequence: 204 | - Authenticate the service principal with the Power BI service. 205 | - Generate an embed token for the report page to be tested. 206 | - Verify that the rendered report does not contain broken visuals. 207 | 208 | ![Example of tests running](./documentation/images/Figure3.gif) 209 | 210 | ## Reading the Results 211 | 212 | 1. **Open the `playwright-report` Folder** 213 | After the tests have run, open the `playwright-report` folder generated by Playwright. 214 | 215 | 2. **View the Report** 216 | Double-click on `index.html` to open the report in a web browser. This report will show detailed test results. 217 | 218 | ![Figure 4](./documentation/images/Figure4.png) 219 | 220 | ## Broken Visuals 221 | 222 | This testing tool will look for various issues in Power BI visuals as described in the official documentation on troubleshooting tile errors. The types of errors detected include: 223 | 224 | 1. Power BI encountered an unexpected error while loading the model. 225 | 226 | 2. Couldn't retrieve the data model. 227 | 228 | 3. You don't have permission to view this tile or open the workbook. 229 | 230 | 4. Power BI visuals have been disabled by your administrator. 231 | 232 | 5. Data shapes must contain at least one group or calculation that outputs data. 233 | 234 | 6. Can't display the data because Power BI can't determine the relationship between two or more fields. 235 | 236 | 7. The groups in the primary axis and the secondary axis overlap. Groups in the primary axis can't have the same keys as groups in the secondary axis. 237 | 238 | 8. This visual has exceeded the available resources. 239 | 240 | 9. We are not able to identify the following fields: {0}. 241 | 242 | 10. Couldn't retrieve the data for this visual. 243 | 244 | ## Continuous Integration 245 | 246 | To automate these tests in Azure DevOps please see these [instructions](/documentation/ci-mode.md) 247 | 248 | ## Limitations 249 | 250 | 1. Testing reports that use [composite models](https://learn.microsoft.com/en-us/power-bi/transform-model/desktop-composite-models) does not work due to a limitation with Microsoft's [PowerBI-JavaScript](https://github.com/microsoft/PowerBI-JavaScript) library. Trying to test reports that use composite models will always indicate a broken visual with this tool. -------------------------------------------------------------------------------- /documentation/ci-mode.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration 2 | 3 | You can automate the testing for broken visuals using Azure DevOps. Please see the instructions below to set up a pipeline. 4 | 5 | ## Table of Contents 6 | 7 | - [Continuous Integration](#continuous-integration) 8 | - [Table of Contents](#table-of-contents) 9 | - [High-Level Process](#high-level-process) 10 | - [Prerequisites](#prerequisites) 11 | - [Instructions](#instructions) 12 | - [Clone Repo](#clone-repo) 13 | - [Local Test Results](#local-test-results) 14 | - [Create the Variable Group](#create-the-variable-group) 15 | - [Create the Pipeline](#create-the-pipeline) 16 | - [Monitoring](#monitoring) 17 | 18 | 19 | ## High-Level Process 20 | 21 | ![Figure 1](./images/CI-Figure1.png) 22 | *Figure 1 -- High-level diagram of automated testing for broken visuals* 23 | 24 | In the visual depicted in Figure 1, the CSV files for the tests to be conducted are stored in the repository. They can be generated using the PowerShell module as described in [generating test cases](../README.md#4-add-test-cases). 25 | 26 | An Azure Pipeline example is provided and is triggered to run every six hours, but you can update it for your testing purposes. 27 | 28 | ## Prerequisites 29 | 30 | 1. You have an Azure DevOps project and have at least Project or Build Administrator rights for that project. 31 | 32 | 2. You have connected a Premium-backed capacity workspace to your repository in your Azure DevOps project. Instructions are provided [at this link](https://learn.microsoft.com/en-us/power-bi/developer/projects/projects-git). 33 | 34 | 3. Your Power BI tenant has [XMLA Read/Write Enabled](https://learn.microsoft.com/en-us/power-bi/enterprise/service-premium-connect-tools#enable-xmla-read-write). 35 | 36 | 4. A service principal is required to authenticate and perform actions in the Power BI service. 37 | - Go to the Azure portal to register a new application and create a service principal. 38 | - Provide the following API permissions with admin consent granted: 39 | - `App.Read.All` 40 | - `Dataset.Read.All` 41 | - `SemanticModel.Read.All` 42 | - `Report.Read.All` 43 | - `Workspace.Read.All` 44 | - Create a secret for the service principal. 45 | - Note down the **Client ID**, **Tenant ID**, and **Client Secret**, as these will be needed for configuration later. 46 | 47 | 5. Ensure that the workspaces being tested have set the service principal to have **Member** rights to the workspaces. 48 | 49 | ## Instructions 50 | 51 | ### Clone Repo 52 | 53 | 1. Import the repository into your Azure DevOps project. Instructions are [provided here](https://learn.microsoft.com/en-us/azure/devops/repos/git/import-git-repository?view=azure-devops). The Clone URL is "https://github.com/kerski/pbi-dataops-visual-error-testing.git". 54 | 55 | ### Local Test Results 56 | 57 | 1. Put the CSV files for testing into the test-cases folder. 58 | 59 | ### Create the Variable Group 60 | 61 | 1. In your project, navigate to the **Pipelines -> Library** section. 62 | 63 | ![Variable Groups](../documentation/images/automated-testing-library.png) 64 | 65 | 2. Select the "Add Variable Group" button. 66 | 67 | ![Add Variable Group](../documentation/images/automated-testing-variable-group.png) 68 | 69 | 3. Create a variable group called "VisualTests" and add the following variables: 70 | 71 | - `CI` - Set to true and helps with logging of test results. 72 | - `CLIENT_ID` - The service principal's application/client ID. 73 | - `CLIENT_SECRET` - The client secret for the service principal. 74 | - `ENVIRONMENT` - The region your tenant is located in. The values can be Public, Germany, China, USGov, USGovHigh, or USGovDoD. 75 | - `TENANT_ID` - The Tenant GUID. You can locate it by following the instructions [at this link](https://learn.microsoft.com/en-us/sharepoint/find-your-office-365-tenant-id). 76 | 77 | ![Create Variable Group](../documentation/images/automated-testing-create-variable-group.png) 78 | 79 | 4. Save the variable group. 80 | 81 | ![Save Variable Group](../documentation/images/automated-testing-save-variable-group.png) 82 | 83 | ### Create the Pipeline 84 | 85 | 1. Navigate to the pipeline interface. 86 | 87 | ![Navigate to Pipeline](../documentation/images/automated-testing-navigate-pipeline.png) 88 | 89 | 2. Select the "New Pipeline" button. 90 | 91 | ![New Pipeline](../documentation/images/automated-testing-create-pipeline.png) 92 | 93 | 3. Select the **Azure Repos Git** option. 94 | 95 | ![ADO Option](../documentation/images/automated-testing-ado-option.png) 96 | 97 | 4. Select the repository you imported via the import process. 98 | 99 | ![Select Repo](../documentation/images/automated-testing-select-repo.png) 100 | 101 | 5. In the "Configure your pipeline" screen, select **Existing Azure Pipelines YAML file**. 102 | 103 | ![Configure your pipeline](../documentation/images/configure-your-pipeline.png) 104 | 105 | 6. Select the branch 'main' and the path `/pipeline-scripts/playwright-docker-template.yml`. Select the **Continue** button. 106 | 107 | ![Select an existing YAML file](../documentation/images/select-an-existing-YAML-file.png) 108 | 109 | 7. Select the "Run" button. If you make any updates to the triggers, you may see a "Save and Run" button. 110 | 111 | ![Save and Run](../documentation/images/review-your-pipeline-YAML.png) 112 | 113 | 8. You will be prompted to commit to the main branch. Select the **Save and Run** button. 114 | 115 | ![Save and Run again](../documentation/images/automated-testing-save-and-run.png) 116 | 117 | 9. You will be redirected to the first pipeline run, and you will be asked to authorize the pipeline to access the variable group created previously. Select the **View** button. 118 | 119 | 10. A pop-up window will appear. Select the **View** button. 120 | 121 | ![Permit](../documentation/images/automated-testing-permit.png) 122 | 123 | 11. You will be asked to confirm. Select the **Permit** button. 124 | 125 | ![Permit Again](../documentation/images/automated-testing-permit-again.png) 126 | 127 | 12. This will kick off the automated tests. 128 | 129 | ![Run Tests Console](../documentation/images/run-playwright-tests.png) 130 | 131 | 13. For any failed tests, this will be logged to the job, and the pipeline will also fail. 132 | 133 | ![Failed Tests](../documentation/images/failed-tests.png) 134 | 135 | ## Monitoring 136 | 137 | It's essential to monitor the Azure DevOps pipeline for any failures. I've also written about some best practices for setting that up [in this article](https://www.kerski.tech/bringing-dataops-to-power-bi-part31/). 138 | -------------------------------------------------------------------------------- /documentation/images/CI-Figure1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/CI-Figure1.png -------------------------------------------------------------------------------- /documentation/images/Figure3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/Figure3.gif -------------------------------------------------------------------------------- /documentation/images/Figure4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/Figure4.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-ado-option.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-ado-option.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-create-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-create-pipeline.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-create-variable-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-create-variable-group.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-library.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-navigate-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-navigate-pipeline.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-permit-again.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-permit-again.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-permit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-permit.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-save-and-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-save-and-run.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-save-variable-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-save-variable-group.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-select-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-select-repo.png -------------------------------------------------------------------------------- /documentation/images/automated-testing-variable-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/automated-testing-variable-group.png -------------------------------------------------------------------------------- /documentation/images/configure-your-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/configure-your-pipeline.png -------------------------------------------------------------------------------- /documentation/images/failed-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/failed-tests.png -------------------------------------------------------------------------------- /documentation/images/rdl-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/rdl-error.png -------------------------------------------------------------------------------- /documentation/images/rdl-multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/rdl-multi.png -------------------------------------------------------------------------------- /documentation/images/rdl-test-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/rdl-test-file.png -------------------------------------------------------------------------------- /documentation/images/review-your-pipeline-YAML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/review-your-pipeline-YAML.png -------------------------------------------------------------------------------- /documentation/images/run-playwright-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/run-playwright-tests.png -------------------------------------------------------------------------------- /documentation/images/select-an-existing-YAML-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/select-an-existing-YAML-file.png -------------------------------------------------------------------------------- /documentation/images/test-cases.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/test-cases.jpg -------------------------------------------------------------------------------- /documentation/images/ui-test-generation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerski/pbi-dataops-visual-error-testing/2b61ea34ad5cba72cc2c7bc8cd86c935c12da22a/documentation/images/ui-test-generation.png -------------------------------------------------------------------------------- /generate-test-cases.ps1: -------------------------------------------------------------------------------- 1 | # Install the module, run once 2 | # Check if the module is already installed 3 | $moduleName = "Get-PowerBIReportPagesForTesting" 4 | if (-not (Get-Module -ListAvailable -Name $moduleName)) { 5 | # Module is not installed, so install it 6 | Write-Output "$moduleName module is not installed. Installing now..." 7 | Install-Module -Name $moduleName -AllowPrerelease -Force -Scope CurrentUser 8 | } else { 9 | # Module is installed 10 | Write-Output "$moduleName module is already installed." 11 | } 12 | $moduleName = "Az.Accounts" 13 | if (-not (Get-Module -ListAvailable -Name $moduleName)) { 14 | # Module is not installed, so install it 15 | Write-Output "$moduleName module is not installed. Installing now..." 16 | Install-Module -Name $moduleName -Force -Scope CurrentUser 17 | } else { 18 | # Module is installed 19 | Write-Output "$moduleName module is already installed." 20 | } 21 | # Import Modules 22 | Import-Module Az.Accounts 23 | Import-Module Get-PowerBIReportPagesForTesting 24 | 25 | # Get Current Credential of the User 26 | $clientId = Read-Host "Enter Client Id" 27 | $clientSecret = Read-Host -AsSecureString "Enter Password" 28 | $tenantId = Read-Host "Enter Tenant Id" 29 | $credential = New-Object System.Management.Automation.PSCredential ($clientId, $clientSecret) 30 | Connect-AzAccount -Credential $credential -ServicePrincipal 31 | 32 | # Get other datasets 33 | $datasetId = Read-Host "Provide the Dataset Id/Semantic Model Id" 34 | $workspaceId = Read-Host "Provide the Workspace Id" 35 | $roleUserName = Read-Host "Provide the Role User Name if using RLS with this semantic model" 36 | 37 | Get-PowerBIReportPagesForTesting -DatasetId $datasetId ` 38 | -WorkspaceId $workspaceId ` 39 | -WorkspaceIdsToCheck $workspaceId ` 40 | -Credential $credential ` 41 | -TenantId $tenantId ` 42 | -LogOutput "Host" ` 43 | -Environment Public ` 44 | -RoleUserName $roleUserName ` 45 | -Path ".\test-cases\test.csv" -------------------------------------------------------------------------------- /global-setup.ts: -------------------------------------------------------------------------------- 1 | import type { FullConfig } from '@playwright/test'; 2 | 3 | 4 | /** 5 | * Global setup function for Playwright tests. 6 | * 7 | * @param {FullConfig} config - The full configuration object for Playwright. 8 | * @returns {Promise} A promise that resolves when the global setup is complete. 9 | */ 10 | async function globalSetup(config: FullConfig): Promise { 11 | console.log("##[debug]Global setup initiated"); 12 | console.log("##[debug]CI: " + process.env.CI); 13 | } 14 | 15 | export default globalSetup; -------------------------------------------------------------------------------- /helper-functions/file-reader.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { parse } from 'csv-parse/sync'; 4 | 5 | // Function to read all CSV files from a folder 6 | export function readCSVFilesFromFolder(folderPath: string): any { 7 | let tempRecords: Array = []; 8 | // Read the directory 9 | fs.readdirSync(folderPath).forEach(file => { 10 | if(path.extname(file) === '.csv'){ 11 | let x = parse(fs.readFileSync(path.join(folderPath, file)), { 12 | columns: true, 13 | skip_empty_lines: true 14 | }); 15 | // load results 16 | for(let i = 0; i < x.length; i++){ 17 | tempRecords.push(x[i]); 18 | }// end for 19 | }//end file extension check 20 | }); 21 | return tempRecords; 22 | } 23 | 24 | // Function to read all JSON files from a folder 25 | export function readJSONFilesFromFolder(folderPath: string): any { 26 | let tempRecords: Array = []; 27 | // Read the directory 28 | fs.readdirSync(folderPath).forEach(file => { 29 | // Check if the file is a JSON file 30 | if (path.extname(file) === '.json') { 31 | let data = fs.readFileSync(path.join(folderPath, file), 'utf8'); 32 | let json = JSON.parse(data); 33 | for (let i = 0; i < json.length; i++) { 34 | // Concatenate the name properties in the report_parameters array 35 | if (json[i].report_parameters) { 36 | json[i].report_parameters_string = json[i].report_parameters.map(param => `rp:${encodeURIComponent(param.name)}=${encodeURIComponent(param.value)}`).join('&'); 37 | } 38 | else{ 39 | json[i].report_parameters_string = ''; 40 | } 41 | }// end for 42 | tempRecords.push(json); 43 | } // end file extension check 44 | }); 45 | // Flatten the tempRecords array 46 | return tempRecords.flat(); 47 | } 48 | -------------------------------------------------------------------------------- /helper-functions/logging.ts: -------------------------------------------------------------------------------- 1 | // Function to log if verbose turned on 2 | export function logToConsole(message: any, isVerboseLogging: boolean): any { 3 | if(isVerboseLogging){ 4 | console.log(message); 5 | }// end if 6 | } 7 | 8 | -------------------------------------------------------------------------------- /helper-functions/token-helpers.ts: -------------------------------------------------------------------------------- 1 | // Importing required modules for authentication and HTTP requests 2 | import { ConfidentialClientApplication } from '@azure/msal-node'; 3 | import axios from 'axios'; 4 | 5 | // Interfaces for configuration and embedding 6 | export interface Endpoints { 7 | apiPrefix: string; 8 | webPrefix: string; 9 | resourceUrl: string; 10 | embedUrl: string; 11 | loginUrl: string; 12 | } 13 | 14 | export interface EmbedInfo { 15 | workspaceId: string; 16 | reportId: string; 17 | bookmarkId?: string; 18 | pageId: string; 19 | userName: string; 20 | datasetId: string; 21 | role: string; 22 | } 23 | 24 | export interface ReportEmbedInfo { 25 | reports: any[]; 26 | datasets: any[]; 27 | targetWorkspaces: any[]; 28 | accessLevel: string; 29 | identities?: any[]; 30 | } 31 | 32 | export interface PaginatedEmbedInfo { 33 | reports: any; 34 | datasets: []; 35 | } 36 | 37 | export interface TestSettings { 38 | clientId: string; 39 | clientSecret: string; 40 | tenantId: string; 41 | environment: string; 42 | testCases: string; 43 | } 44 | 45 | // Enum for supported cloud environments 46 | export enum Environment { 47 | Public = "Public", 48 | Germany = "Germany", 49 | China = "China", 50 | USGov = "USGov", 51 | USGovHigh = "USGovHigh", 52 | USGovDoD = "USGovDoD" 53 | } 54 | 55 | /** 56 | * Returns the correct API endpoints for the specified Power BI environment. 57 | */ 58 | export function getAPIEndpoints(environment: Environment): Endpoints { 59 | // Default to Public cloud endpoints 60 | let endpoints: Endpoints = { 61 | apiPrefix: 'https://api.powerbi.com', 62 | webPrefix: 'https://app.powerbi.com', 63 | resourceUrl: 'https://analysis.windows.net/powerbi/api', 64 | embedUrl: 'https://app.powerbi.com/reportEmbed', 65 | loginUrl: 'https://login.microsoftonline.com' 66 | }; 67 | 68 | // Override endpoints for other sovereign clouds 69 | switch (environment) { 70 | case Environment.Germany: 71 | endpoints.apiPrefix = "https://api.powerbi.de"; 72 | endpoints.webPrefix = "https://app.powerbi.de"; 73 | endpoints.resourceUrl = "https://analysis.cloudapi.de/powerbi/api"; 74 | endpoints.embedUrl = "https://app.powerbi.de/reportEmbed"; 75 | break; 76 | case Environment.China: 77 | endpoints.apiPrefix = "https://api.powerbi.cn"; 78 | endpoints.webPrefix = "https://app.powerbigov.cn"; 79 | endpoints.resourceUrl = "https://analysis.chinacloudapi.cn/powerbi/api"; 80 | endpoints.embedUrl = "https://app.powerbi.cn/reportEmbed"; 81 | endpoints.loginUrl = "https://login.partner.microsoftonline.cn"; 82 | break; 83 | case Environment.USGov: 84 | endpoints.apiPrefix = "https://api.powerbigov.us"; 85 | endpoints.webPrefix = "https://app.powerbigov.us"; 86 | endpoints.resourceUrl = "https://analysis.usgovcloudapi.net/powerbi/api"; 87 | endpoints.embedUrl = "https://app.powerbigov.us/reportEmbed"; 88 | break; 89 | case Environment.USGovHigh: 90 | endpoints.apiPrefix = "https://api.high.powerbigov.us"; 91 | endpoints.webPrefix = "https://app.high.powerbigov.us"; 92 | endpoints.resourceUrl = "https://analysis.high.usgovcloudapi.net/powerbi/api"; 93 | endpoints.embedUrl = "https://app.high.powerbigov.us/reportEmbed"; 94 | endpoints.loginUrl = "https://login.microsoftonline.us"; 95 | break; 96 | case Environment.USGovDoD: 97 | endpoints.apiPrefix = "https://api.mil.powerbi.us"; 98 | endpoints.webPrefix = "https://app.mil.powerbi.us"; 99 | endpoints.resourceUrl = "https://analysis.dod.usgovcloudapi.net/powerbi/api"; 100 | endpoints.embedUrl = "https://app.mil.powerbi.us/reportEmbed"; 101 | endpoints.loginUrl = "https://login.microsoftonline.us"; 102 | break; 103 | } 104 | 105 | return endpoints; 106 | } 107 | 108 | /** 109 | * Acquires an Azure AD token using client credentials. 110 | */ 111 | export async function getAccessToken(testSettings: TestSettings): Promise { 112 | const endpoint = getAPIEndpoints(testSettings.environment as Environment); 113 | 114 | // Configure the confidential client for MSAL 115 | const cca = new ConfidentialClientApplication({ 116 | auth: { 117 | clientId: testSettings.clientId, 118 | clientSecret: testSettings.clientSecret, 119 | authority: `${endpoint.loginUrl}/${testSettings.tenantId}` 120 | } 121 | }); 122 | 123 | // Request token with scope for Power BI 124 | const tokenRequest = { scopes: [`${endpoint.resourceUrl}/.default`] }; 125 | 126 | try { 127 | const response = await cca.acquireTokenByClientCredential(tokenRequest); 128 | return response?.accessToken; 129 | } catch (error) { 130 | console.error(`Failed to get access token:`, error); 131 | } 132 | } 133 | 134 | /** 135 | * Requests an embed token for a report. 136 | */ 137 | export async function getReportEmbedToken(embedInfo: ReportEmbedInfo, endpoint: Endpoints, accessToken: string): Promise { 138 | const url = `${endpoint.apiPrefix}/v1.0/myorg/GenerateToken`; 139 | const headers = { 140 | 'Content-Type': 'application/json; charset=utf-8', 141 | 'Authorization': `Bearer ${accessToken}` 142 | }; 143 | 144 | try { 145 | const response = await axios.post(url, embedInfo, { headers }); 146 | return response.data.token; 147 | } catch (error: any) { 148 | console.error(`Failed to get embed token:`, error); 149 | if (error.response) { 150 | console.error('Error Status:', error.response.status); 151 | console.error('Error Data:', error.response.data); 152 | } else if (error.request) { 153 | console.error('No response received:', error.request); 154 | } else { 155 | console.error('Error setting up request:', error.message); 156 | } 157 | } 158 | } 159 | 160 | /** 161 | * Requests an embed token for paginated report scenarios. 162 | */ 163 | export async function getPaginatedEmbedToken(embedInfo: PaginatedEmbedInfo, endpoint: Endpoints, accessToken: string): Promise { 164 | const url = `${endpoint.apiPrefix}/v1.0/myorg/GenerateToken`; 165 | const headers = { 166 | 'Content-Type': 'application/json; charset=utf-8', 167 | 'Authorization': `Bearer ${accessToken}` 168 | }; 169 | 170 | try { 171 | const response = await axios.post(url, embedInfo, { headers }); 172 | return response.data.token; 173 | } catch (error: any) { 174 | console.error(`Failed to get embed token:`, error); 175 | } 176 | } 177 | 178 | /** 179 | * Creates a standardized embed payload for a report from a record. 180 | */ 181 | export function createReportEmbedInfo(record: any): ReportEmbedInfo { 182 | const embedInfo: ReportEmbedInfo = { 183 | reports: [{ id: record.report_id }], 184 | datasets: [{ id: record.dataset_id }], 185 | targetWorkspaces: [{ id: record.workspace_id }], 186 | accessLevel: 'View' 187 | }; 188 | 189 | // If RLS is needed, set the identity 190 | if (record.user_name) { 191 | embedInfo.identities = [ 192 | { 193 | username: record.user_name, 194 | roles: [record.role], 195 | datasets: [record.dataset_id] 196 | } 197 | ]; 198 | } 199 | 200 | return embedInfo; 201 | } 202 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pbi-dataops-visual-error-testing", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "pbi-dataops-visual-error-testing", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@azure/msal-node": "^2.9.0", 13 | "asynckit": "^0.4.0", 14 | "axios": "^1.7.2", 15 | "buffer-equal-constant-time": "^1.0.1", 16 | "combined-stream": "^1.0.8", 17 | "csv-parse": "^5.5.6", 18 | "delayed-stream": "^1.0.0", 19 | "dotenv": "^16.4.5", 20 | "ecdsa-sig-formatter": "^1.0.11", 21 | "es6-promise": "^3.3.1", 22 | "follow-redirects": "^1.15.6", 23 | "form-data": "^4.0.0", 24 | "fs": "^0.0.1-security", 25 | "http-post-message": "^0.2.3", 26 | "inherits": "^2.0.3", 27 | "jsonwebtoken": "^9.0.2", 28 | "jwa": "^1.4.1", 29 | "jws": "^3.2.2", 30 | "lodash.includes": "^4.3.0", 31 | "lodash.isboolean": "^3.0.3", 32 | "lodash.isinteger": "^4.0.4", 33 | "lodash.isnumber": "^3.0.3", 34 | "lodash.isplainobject": "^4.0.6", 35 | "lodash.isstring": "^4.0.1", 36 | "lodash.once": "^4.1.1", 37 | "mime-db": "^1.52.0", 38 | "mime-types": "^2.1.35", 39 | "ms": "^2.1.3", 40 | "path": "^0.12.7", 41 | "playwright": "^1.47.2", 42 | "playwright-core": "^1.44.1", 43 | "powerbi-client": "^2.23.1", 44 | "powerbi-models": "^1.15.2", 45 | "powerbi-router": "^0.1.5", 46 | "process": "^0.11.10", 47 | "proxy-from-env": "^1.1.0", 48 | "route-recognizer": "^0.1.11", 49 | "safe-buffer": "^5.2.1", 50 | "semver": "^7.6.2", 51 | "undici-types": "^5.26.5", 52 | "util": "^0.10.4", 53 | "uuid": "^8.3.2", 54 | "window-post-message-proxy": "^0.2.8" 55 | }, 56 | "devDependencies": { 57 | "@azure/microsoft-playwright-testing": "^1.0.0-beta.7", 58 | "@playwright/test": "^1.48.2", 59 | "@types/node": "^20.13.0" 60 | } 61 | }, 62 | "node_modules/@azure/abort-controller": { 63 | "version": "2.1.2", 64 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", 65 | "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", 66 | "dev": true, 67 | "license": "MIT", 68 | "dependencies": { 69 | "tslib": "^2.6.2" 70 | }, 71 | "engines": { 72 | "node": ">=18.0.0" 73 | } 74 | }, 75 | "node_modules/@azure/core-auth": { 76 | "version": "1.9.0", 77 | "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", 78 | "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", 79 | "dev": true, 80 | "license": "MIT", 81 | "dependencies": { 82 | "@azure/abort-controller": "^2.0.0", 83 | "@azure/core-util": "^1.11.0", 84 | "tslib": "^2.6.2" 85 | }, 86 | "engines": { 87 | "node": ">=18.0.0" 88 | } 89 | }, 90 | "node_modules/@azure/core-client": { 91 | "version": "1.9.2", 92 | "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", 93 | "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", 94 | "dev": true, 95 | "license": "MIT", 96 | "dependencies": { 97 | "@azure/abort-controller": "^2.0.0", 98 | "@azure/core-auth": "^1.4.0", 99 | "@azure/core-rest-pipeline": "^1.9.1", 100 | "@azure/core-tracing": "^1.0.0", 101 | "@azure/core-util": "^1.6.1", 102 | "@azure/logger": "^1.0.0", 103 | "tslib": "^2.6.2" 104 | }, 105 | "engines": { 106 | "node": ">=18.0.0" 107 | } 108 | }, 109 | "node_modules/@azure/core-http-compat": { 110 | "version": "2.1.2", 111 | "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", 112 | "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", 113 | "dev": true, 114 | "license": "MIT", 115 | "dependencies": { 116 | "@azure/abort-controller": "^2.0.0", 117 | "@azure/core-client": "^1.3.0", 118 | "@azure/core-rest-pipeline": "^1.3.0" 119 | }, 120 | "engines": { 121 | "node": ">=18.0.0" 122 | } 123 | }, 124 | "node_modules/@azure/core-lro": { 125 | "version": "2.7.2", 126 | "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", 127 | "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", 128 | "dev": true, 129 | "license": "MIT", 130 | "dependencies": { 131 | "@azure/abort-controller": "^2.0.0", 132 | "@azure/core-util": "^1.2.0", 133 | "@azure/logger": "^1.0.0", 134 | "tslib": "^2.6.2" 135 | }, 136 | "engines": { 137 | "node": ">=18.0.0" 138 | } 139 | }, 140 | "node_modules/@azure/core-paging": { 141 | "version": "1.6.2", 142 | "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", 143 | "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", 144 | "dev": true, 145 | "license": "MIT", 146 | "dependencies": { 147 | "tslib": "^2.6.2" 148 | }, 149 | "engines": { 150 | "node": ">=18.0.0" 151 | } 152 | }, 153 | "node_modules/@azure/core-rest-pipeline": { 154 | "version": "1.17.0", 155 | "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.17.0.tgz", 156 | "integrity": "sha512-62Vv8nC+uPId3j86XJ0WI+sBf0jlqTqPUFCBNrGtlaUeQUIXWV/D8GE5A1d+Qx8H7OQojn2WguC8kChD6v0shA==", 157 | "dev": true, 158 | "license": "MIT", 159 | "dependencies": { 160 | "@azure/abort-controller": "^2.0.0", 161 | "@azure/core-auth": "^1.8.0", 162 | "@azure/core-tracing": "^1.0.1", 163 | "@azure/core-util": "^1.9.0", 164 | "@azure/logger": "^1.0.0", 165 | "http-proxy-agent": "^7.0.0", 166 | "https-proxy-agent": "^7.0.0", 167 | "tslib": "^2.6.2" 168 | }, 169 | "engines": { 170 | "node": ">=18.0.0" 171 | } 172 | }, 173 | "node_modules/@azure/core-tracing": { 174 | "version": "1.2.0", 175 | "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", 176 | "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", 177 | "dev": true, 178 | "license": "MIT", 179 | "dependencies": { 180 | "tslib": "^2.6.2" 181 | }, 182 | "engines": { 183 | "node": ">=18.0.0" 184 | } 185 | }, 186 | "node_modules/@azure/core-util": { 187 | "version": "1.11.0", 188 | "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", 189 | "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", 190 | "dev": true, 191 | "license": "MIT", 192 | "dependencies": { 193 | "@azure/abort-controller": "^2.0.0", 194 | "tslib": "^2.6.2" 195 | }, 196 | "engines": { 197 | "node": ">=18.0.0" 198 | } 199 | }, 200 | "node_modules/@azure/core-xml": { 201 | "version": "1.4.4", 202 | "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.4.tgz", 203 | "integrity": "sha512-J4FYAqakGXcbfeZjwjMzjNcpcH4E+JtEBv+xcV1yL0Ydn/6wbQfeFKTCHh9wttAi0lmajHw7yBbHPRG+YHckZQ==", 204 | "dev": true, 205 | "license": "MIT", 206 | "dependencies": { 207 | "fast-xml-parser": "^4.4.1", 208 | "tslib": "^2.6.2" 209 | }, 210 | "engines": { 211 | "node": ">=18.0.0" 212 | } 213 | }, 214 | "node_modules/@azure/identity": { 215 | "version": "4.5.0", 216 | "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.5.0.tgz", 217 | "integrity": "sha512-EknvVmtBuSIic47xkOqyNabAme0RYTw52BTMz8eBgU1ysTyMrD1uOoM+JdS0J/4Yfp98IBT3osqq3BfwSaNaGQ==", 218 | "dev": true, 219 | "license": "MIT", 220 | "dependencies": { 221 | "@azure/abort-controller": "^2.0.0", 222 | "@azure/core-auth": "^1.9.0", 223 | "@azure/core-client": "^1.9.2", 224 | "@azure/core-rest-pipeline": "^1.17.0", 225 | "@azure/core-tracing": "^1.0.0", 226 | "@azure/core-util": "^1.11.0", 227 | "@azure/logger": "^1.0.0", 228 | "@azure/msal-browser": "^3.26.1", 229 | "@azure/msal-node": "^2.15.0", 230 | "events": "^3.0.0", 231 | "jws": "^4.0.0", 232 | "open": "^8.0.0", 233 | "stoppable": "^1.1.0", 234 | "tslib": "^2.2.0" 235 | }, 236 | "engines": { 237 | "node": ">=18.0.0" 238 | } 239 | }, 240 | "node_modules/@azure/identity/node_modules/jwa": { 241 | "version": "2.0.0", 242 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", 243 | "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", 244 | "dev": true, 245 | "license": "MIT", 246 | "dependencies": { 247 | "buffer-equal-constant-time": "1.0.1", 248 | "ecdsa-sig-formatter": "1.0.11", 249 | "safe-buffer": "^5.0.1" 250 | } 251 | }, 252 | "node_modules/@azure/identity/node_modules/jws": { 253 | "version": "4.0.0", 254 | "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", 255 | "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", 256 | "dev": true, 257 | "license": "MIT", 258 | "dependencies": { 259 | "jwa": "^2.0.0", 260 | "safe-buffer": "^5.0.1" 261 | } 262 | }, 263 | "node_modules/@azure/logger": { 264 | "version": "1.1.4", 265 | "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", 266 | "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", 267 | "dev": true, 268 | "license": "MIT", 269 | "dependencies": { 270 | "tslib": "^2.6.2" 271 | }, 272 | "engines": { 273 | "node": ">=18.0.0" 274 | } 275 | }, 276 | "node_modules/@azure/microsoft-playwright-testing": { 277 | "version": "1.0.0-beta.7", 278 | "resolved": "https://registry.npmjs.org/@azure/microsoft-playwright-testing/-/microsoft-playwright-testing-1.0.0-beta.7.tgz", 279 | "integrity": "sha512-Y6C35LWUfLevHu5NG+7vvFfhpmUrGWKRumcz7/CSCmWlx8RVfWgP6NuL8rIPDuTeJyjaTczNfeg1ppGW26TjBw==", 280 | "dev": true, 281 | "license": "MIT", 282 | "dependencies": { 283 | "@azure/core-rest-pipeline": "^1.16.3", 284 | "@azure/identity": "^4.3.1", 285 | "@azure/logger": "^1.1.4", 286 | "@azure/storage-blob": "^12.15.0", 287 | "tslib": "^2.6.0" 288 | }, 289 | "engines": { 290 | "node": ">=18.0.0" 291 | }, 292 | "peerDependencies": { 293 | "@playwright/test": "^1.43.1" 294 | } 295 | }, 296 | "node_modules/@azure/msal-browser": { 297 | "version": "3.26.1", 298 | "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.26.1.tgz", 299 | "integrity": "sha512-y78sr9g61aCAH9fcLO1um+oHFXc1/5Ap88RIsUSuzkm0BHzFnN+PXGaQeuM1h5Qf5dTnWNOd6JqkskkMPAhh7Q==", 300 | "dev": true, 301 | "license": "MIT", 302 | "dependencies": { 303 | "@azure/msal-common": "14.15.0" 304 | }, 305 | "engines": { 306 | "node": ">=0.8.0" 307 | } 308 | }, 309 | "node_modules/@azure/msal-common": { 310 | "version": "14.15.0", 311 | "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", 312 | "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", 313 | "license": "MIT", 314 | "engines": { 315 | "node": ">=0.8.0" 316 | } 317 | }, 318 | "node_modules/@azure/msal-node": { 319 | "version": "2.15.0", 320 | "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.15.0.tgz", 321 | "integrity": "sha512-gVPW8YLz92ZeCibQH2QUw96odJoiM3k/ZPH3f2HxptozmH6+OnyyvKXo/Egg39HAM230akarQKHf0W74UHlh0Q==", 322 | "license": "MIT", 323 | "dependencies": { 324 | "@azure/msal-common": "14.15.0", 325 | "jsonwebtoken": "^9.0.0", 326 | "uuid": "^8.3.0" 327 | }, 328 | "engines": { 329 | "node": ">=16" 330 | } 331 | }, 332 | "node_modules/@azure/storage-blob": { 333 | "version": "12.25.0", 334 | "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.25.0.tgz", 335 | "integrity": "sha512-oodouhA3nCCIh843tMMbxty3WqfNT+Vgzj3Xo5jqR9UPnzq3d7mzLjlHAYz7lW+b4km3SIgz+NAgztvhm7Z6kQ==", 336 | "dev": true, 337 | "license": "MIT", 338 | "dependencies": { 339 | "@azure/abort-controller": "^2.1.2", 340 | "@azure/core-auth": "^1.4.0", 341 | "@azure/core-client": "^1.6.2", 342 | "@azure/core-http-compat": "^2.0.0", 343 | "@azure/core-lro": "^2.2.0", 344 | "@azure/core-paging": "^1.1.1", 345 | "@azure/core-rest-pipeline": "^1.10.1", 346 | "@azure/core-tracing": "^1.1.2", 347 | "@azure/core-util": "^1.6.1", 348 | "@azure/core-xml": "^1.4.3", 349 | "@azure/logger": "^1.0.0", 350 | "events": "^3.0.0", 351 | "tslib": "^2.2.0" 352 | }, 353 | "engines": { 354 | "node": ">=18.0.0" 355 | } 356 | }, 357 | "node_modules/@playwright/test": { 358 | "version": "1.48.2", 359 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", 360 | "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", 361 | "dev": true, 362 | "license": "Apache-2.0", 363 | "dependencies": { 364 | "playwright": "1.48.2" 365 | }, 366 | "bin": { 367 | "playwright": "cli.js" 368 | }, 369 | "engines": { 370 | "node": ">=18" 371 | } 372 | }, 373 | "node_modules/@types/node": { 374 | "version": "20.13.0", 375 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.13.0.tgz", 376 | "integrity": "sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ==", 377 | "dev": true, 378 | "dependencies": { 379 | "undici-types": "~5.26.4" 380 | } 381 | }, 382 | "node_modules/agent-base": { 383 | "version": "7.1.1", 384 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", 385 | "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", 386 | "dev": true, 387 | "license": "MIT", 388 | "dependencies": { 389 | "debug": "^4.3.4" 390 | }, 391 | "engines": { 392 | "node": ">= 14" 393 | } 394 | }, 395 | "node_modules/asynckit": { 396 | "version": "0.4.0", 397 | "license": "MIT" 398 | }, 399 | "node_modules/axios": { 400 | "version": "1.8.4", 401 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", 402 | "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", 403 | "license": "MIT", 404 | "dependencies": { 405 | "follow-redirects": "^1.15.6", 406 | "form-data": "^4.0.0", 407 | "proxy-from-env": "^1.1.0" 408 | } 409 | }, 410 | "node_modules/buffer-equal-constant-time": { 411 | "version": "1.0.1", 412 | "license": "BSD-3-Clause" 413 | }, 414 | "node_modules/call-bind-apply-helpers": { 415 | "version": "1.0.2", 416 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 417 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 418 | "license": "MIT", 419 | "dependencies": { 420 | "es-errors": "^1.3.0", 421 | "function-bind": "^1.1.2" 422 | }, 423 | "engines": { 424 | "node": ">= 0.4" 425 | } 426 | }, 427 | "node_modules/combined-stream": { 428 | "version": "1.0.8", 429 | "license": "MIT", 430 | "dependencies": { 431 | "delayed-stream": "~1.0.0" 432 | }, 433 | "engines": { 434 | "node": ">= 0.8" 435 | } 436 | }, 437 | "node_modules/csv-parse": { 438 | "version": "5.5.6", 439 | "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.6.tgz", 440 | "integrity": "sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==" 441 | }, 442 | "node_modules/debug": { 443 | "version": "4.3.7", 444 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 445 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 446 | "dev": true, 447 | "license": "MIT", 448 | "dependencies": { 449 | "ms": "^2.1.3" 450 | }, 451 | "engines": { 452 | "node": ">=6.0" 453 | }, 454 | "peerDependenciesMeta": { 455 | "supports-color": { 456 | "optional": true 457 | } 458 | } 459 | }, 460 | "node_modules/define-lazy-prop": { 461 | "version": "2.0.0", 462 | "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", 463 | "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", 464 | "dev": true, 465 | "license": "MIT", 466 | "engines": { 467 | "node": ">=8" 468 | } 469 | }, 470 | "node_modules/delayed-stream": { 471 | "version": "1.0.0", 472 | "license": "MIT", 473 | "engines": { 474 | "node": ">=0.4.0" 475 | } 476 | }, 477 | "node_modules/dotenv": { 478 | "version": "16.4.5", 479 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 480 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 481 | "engines": { 482 | "node": ">=12" 483 | }, 484 | "funding": { 485 | "url": "https://dotenvx.com" 486 | } 487 | }, 488 | "node_modules/dunder-proto": { 489 | "version": "1.0.1", 490 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 491 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 492 | "license": "MIT", 493 | "dependencies": { 494 | "call-bind-apply-helpers": "^1.0.1", 495 | "es-errors": "^1.3.0", 496 | "gopd": "^1.2.0" 497 | }, 498 | "engines": { 499 | "node": ">= 0.4" 500 | } 501 | }, 502 | "node_modules/ecdsa-sig-formatter": { 503 | "version": "1.0.11", 504 | "license": "Apache-2.0", 505 | "dependencies": { 506 | "safe-buffer": "^5.0.1" 507 | } 508 | }, 509 | "node_modules/es-define-property": { 510 | "version": "1.0.1", 511 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 512 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 513 | "license": "MIT", 514 | "engines": { 515 | "node": ">= 0.4" 516 | } 517 | }, 518 | "node_modules/es-errors": { 519 | "version": "1.3.0", 520 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 521 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 522 | "license": "MIT", 523 | "engines": { 524 | "node": ">= 0.4" 525 | } 526 | }, 527 | "node_modules/es-object-atoms": { 528 | "version": "1.1.1", 529 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 530 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 531 | "license": "MIT", 532 | "dependencies": { 533 | "es-errors": "^1.3.0" 534 | }, 535 | "engines": { 536 | "node": ">= 0.4" 537 | } 538 | }, 539 | "node_modules/es-set-tostringtag": { 540 | "version": "2.1.0", 541 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 542 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 543 | "license": "MIT", 544 | "dependencies": { 545 | "es-errors": "^1.3.0", 546 | "get-intrinsic": "^1.2.6", 547 | "has-tostringtag": "^1.0.2", 548 | "hasown": "^2.0.2" 549 | }, 550 | "engines": { 551 | "node": ">= 0.4" 552 | } 553 | }, 554 | "node_modules/es6-promise": { 555 | "version": "3.3.1", 556 | "license": "MIT" 557 | }, 558 | "node_modules/events": { 559 | "version": "3.3.0", 560 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 561 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 562 | "dev": true, 563 | "license": "MIT", 564 | "engines": { 565 | "node": ">=0.8.x" 566 | } 567 | }, 568 | "node_modules/fast-xml-parser": { 569 | "version": "4.5.0", 570 | "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", 571 | "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", 572 | "dev": true, 573 | "funding": [ 574 | { 575 | "type": "github", 576 | "url": "https://github.com/sponsors/NaturalIntelligence" 577 | }, 578 | { 579 | "type": "paypal", 580 | "url": "https://paypal.me/naturalintelligence" 581 | } 582 | ], 583 | "license": "MIT", 584 | "dependencies": { 585 | "strnum": "^1.0.5" 586 | }, 587 | "bin": { 588 | "fxparser": "src/cli/cli.js" 589 | } 590 | }, 591 | "node_modules/follow-redirects": { 592 | "version": "1.15.6", 593 | "funding": [ 594 | { 595 | "type": "individual", 596 | "url": "https://github.com/sponsors/RubenVerborgh" 597 | } 598 | ], 599 | "license": "MIT", 600 | "engines": { 601 | "node": ">=4.0" 602 | }, 603 | "peerDependenciesMeta": { 604 | "debug": { 605 | "optional": true 606 | } 607 | } 608 | }, 609 | "node_modules/form-data": { 610 | "version": "4.0.4", 611 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", 612 | "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", 613 | "license": "MIT", 614 | "dependencies": { 615 | "asynckit": "^0.4.0", 616 | "combined-stream": "^1.0.8", 617 | "es-set-tostringtag": "^2.1.0", 618 | "hasown": "^2.0.2", 619 | "mime-types": "^2.1.12" 620 | }, 621 | "engines": { 622 | "node": ">= 6" 623 | } 624 | }, 625 | "node_modules/fs": { 626 | "version": "0.0.1-security", 627 | "license": "ISC" 628 | }, 629 | "node_modules/fsevents": { 630 | "version": "2.3.2", 631 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 632 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 633 | "hasInstallScript": true, 634 | "optional": true, 635 | "os": [ 636 | "darwin" 637 | ], 638 | "engines": { 639 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 640 | } 641 | }, 642 | "node_modules/function-bind": { 643 | "version": "1.1.2", 644 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 645 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 646 | "license": "MIT", 647 | "funding": { 648 | "url": "https://github.com/sponsors/ljharb" 649 | } 650 | }, 651 | "node_modules/get-intrinsic": { 652 | "version": "1.3.0", 653 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 654 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 655 | "license": "MIT", 656 | "dependencies": { 657 | "call-bind-apply-helpers": "^1.0.2", 658 | "es-define-property": "^1.0.1", 659 | "es-errors": "^1.3.0", 660 | "es-object-atoms": "^1.1.1", 661 | "function-bind": "^1.1.2", 662 | "get-proto": "^1.0.1", 663 | "gopd": "^1.2.0", 664 | "has-symbols": "^1.1.0", 665 | "hasown": "^2.0.2", 666 | "math-intrinsics": "^1.1.0" 667 | }, 668 | "engines": { 669 | "node": ">= 0.4" 670 | }, 671 | "funding": { 672 | "url": "https://github.com/sponsors/ljharb" 673 | } 674 | }, 675 | "node_modules/get-proto": { 676 | "version": "1.0.1", 677 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 678 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 679 | "license": "MIT", 680 | "dependencies": { 681 | "dunder-proto": "^1.0.1", 682 | "es-object-atoms": "^1.0.0" 683 | }, 684 | "engines": { 685 | "node": ">= 0.4" 686 | } 687 | }, 688 | "node_modules/gopd": { 689 | "version": "1.2.0", 690 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 691 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 692 | "license": "MIT", 693 | "engines": { 694 | "node": ">= 0.4" 695 | }, 696 | "funding": { 697 | "url": "https://github.com/sponsors/ljharb" 698 | } 699 | }, 700 | "node_modules/has-symbols": { 701 | "version": "1.1.0", 702 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 703 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 704 | "license": "MIT", 705 | "engines": { 706 | "node": ">= 0.4" 707 | }, 708 | "funding": { 709 | "url": "https://github.com/sponsors/ljharb" 710 | } 711 | }, 712 | "node_modules/has-tostringtag": { 713 | "version": "1.0.2", 714 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 715 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 716 | "license": "MIT", 717 | "dependencies": { 718 | "has-symbols": "^1.0.3" 719 | }, 720 | "engines": { 721 | "node": ">= 0.4" 722 | }, 723 | "funding": { 724 | "url": "https://github.com/sponsors/ljharb" 725 | } 726 | }, 727 | "node_modules/hasown": { 728 | "version": "2.0.2", 729 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 730 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 731 | "license": "MIT", 732 | "dependencies": { 733 | "function-bind": "^1.1.2" 734 | }, 735 | "engines": { 736 | "node": ">= 0.4" 737 | } 738 | }, 739 | "node_modules/http-post-message": { 740 | "version": "0.2.3", 741 | "license": "MIT", 742 | "dependencies": { 743 | "es6-promise": "^3.2.1" 744 | } 745 | }, 746 | "node_modules/http-proxy-agent": { 747 | "version": "7.0.2", 748 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", 749 | "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", 750 | "dev": true, 751 | "license": "MIT", 752 | "dependencies": { 753 | "agent-base": "^7.1.0", 754 | "debug": "^4.3.4" 755 | }, 756 | "engines": { 757 | "node": ">= 14" 758 | } 759 | }, 760 | "node_modules/https-proxy-agent": { 761 | "version": "7.0.5", 762 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", 763 | "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", 764 | "dev": true, 765 | "license": "MIT", 766 | "dependencies": { 767 | "agent-base": "^7.0.2", 768 | "debug": "4" 769 | }, 770 | "engines": { 771 | "node": ">= 14" 772 | } 773 | }, 774 | "node_modules/inherits": { 775 | "version": "2.0.3", 776 | "license": "ISC" 777 | }, 778 | "node_modules/is-docker": { 779 | "version": "2.2.1", 780 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", 781 | "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", 782 | "dev": true, 783 | "license": "MIT", 784 | "bin": { 785 | "is-docker": "cli.js" 786 | }, 787 | "engines": { 788 | "node": ">=8" 789 | }, 790 | "funding": { 791 | "url": "https://github.com/sponsors/sindresorhus" 792 | } 793 | }, 794 | "node_modules/is-wsl": { 795 | "version": "2.2.0", 796 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", 797 | "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", 798 | "dev": true, 799 | "license": "MIT", 800 | "dependencies": { 801 | "is-docker": "^2.0.0" 802 | }, 803 | "engines": { 804 | "node": ">=8" 805 | } 806 | }, 807 | "node_modules/jsonwebtoken": { 808 | "version": "9.0.2", 809 | "license": "MIT", 810 | "dependencies": { 811 | "jws": "^3.2.2", 812 | "lodash.includes": "^4.3.0", 813 | "lodash.isboolean": "^3.0.3", 814 | "lodash.isinteger": "^4.0.4", 815 | "lodash.isnumber": "^3.0.3", 816 | "lodash.isplainobject": "^4.0.6", 817 | "lodash.isstring": "^4.0.1", 818 | "lodash.once": "^4.0.0", 819 | "ms": "^2.1.1", 820 | "semver": "^7.5.4" 821 | }, 822 | "engines": { 823 | "node": ">=12", 824 | "npm": ">=6" 825 | } 826 | }, 827 | "node_modules/jwa": { 828 | "version": "1.4.1", 829 | "license": "MIT", 830 | "dependencies": { 831 | "buffer-equal-constant-time": "1.0.1", 832 | "ecdsa-sig-formatter": "1.0.11", 833 | "safe-buffer": "^5.0.1" 834 | } 835 | }, 836 | "node_modules/jws": { 837 | "version": "3.2.2", 838 | "license": "MIT", 839 | "dependencies": { 840 | "jwa": "^1.4.1", 841 | "safe-buffer": "^5.0.1" 842 | } 843 | }, 844 | "node_modules/lodash.includes": { 845 | "version": "4.3.0", 846 | "license": "MIT" 847 | }, 848 | "node_modules/lodash.isboolean": { 849 | "version": "3.0.3", 850 | "license": "MIT" 851 | }, 852 | "node_modules/lodash.isinteger": { 853 | "version": "4.0.4", 854 | "license": "MIT" 855 | }, 856 | "node_modules/lodash.isnumber": { 857 | "version": "3.0.3", 858 | "license": "MIT" 859 | }, 860 | "node_modules/lodash.isplainobject": { 861 | "version": "4.0.6", 862 | "license": "MIT" 863 | }, 864 | "node_modules/lodash.isstring": { 865 | "version": "4.0.1", 866 | "license": "MIT" 867 | }, 868 | "node_modules/lodash.once": { 869 | "version": "4.1.1", 870 | "license": "MIT" 871 | }, 872 | "node_modules/math-intrinsics": { 873 | "version": "1.1.0", 874 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 875 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 876 | "license": "MIT", 877 | "engines": { 878 | "node": ">= 0.4" 879 | } 880 | }, 881 | "node_modules/mime-db": { 882 | "version": "1.52.0", 883 | "license": "MIT", 884 | "engines": { 885 | "node": ">= 0.6" 886 | } 887 | }, 888 | "node_modules/mime-types": { 889 | "version": "2.1.35", 890 | "license": "MIT", 891 | "dependencies": { 892 | "mime-db": "1.52.0" 893 | }, 894 | "engines": { 895 | "node": ">= 0.6" 896 | } 897 | }, 898 | "node_modules/ms": { 899 | "version": "2.1.3", 900 | "license": "MIT" 901 | }, 902 | "node_modules/open": { 903 | "version": "8.4.2", 904 | "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", 905 | "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", 906 | "dev": true, 907 | "license": "MIT", 908 | "dependencies": { 909 | "define-lazy-prop": "^2.0.0", 910 | "is-docker": "^2.1.1", 911 | "is-wsl": "^2.2.0" 912 | }, 913 | "engines": { 914 | "node": ">=12" 915 | }, 916 | "funding": { 917 | "url": "https://github.com/sponsors/sindresorhus" 918 | } 919 | }, 920 | "node_modules/path": { 921 | "version": "0.12.7", 922 | "license": "MIT", 923 | "dependencies": { 924 | "process": "^0.11.1", 925 | "util": "^0.10.3" 926 | } 927 | }, 928 | "node_modules/playwright": { 929 | "version": "1.48.2", 930 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", 931 | "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", 932 | "license": "Apache-2.0", 933 | "dependencies": { 934 | "playwright-core": "1.48.2" 935 | }, 936 | "bin": { 937 | "playwright": "cli.js" 938 | }, 939 | "engines": { 940 | "node": ">=18" 941 | }, 942 | "optionalDependencies": { 943 | "fsevents": "2.3.2" 944 | } 945 | }, 946 | "node_modules/playwright-core": { 947 | "version": "1.48.2", 948 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", 949 | "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", 950 | "license": "Apache-2.0", 951 | "bin": { 952 | "playwright-core": "cli.js" 953 | }, 954 | "engines": { 955 | "node": ">=18" 956 | } 957 | }, 958 | "node_modules/powerbi-client": { 959 | "version": "2.23.1", 960 | "license": "MIT", 961 | "dependencies": { 962 | "http-post-message": "^0.2", 963 | "powerbi-models": "^1.14.0", 964 | "powerbi-router": "^0.1", 965 | "window-post-message-proxy": "^0.2.7" 966 | } 967 | }, 968 | "node_modules/powerbi-models": { 969 | "version": "1.15.2", 970 | "license": "MIT" 971 | }, 972 | "node_modules/powerbi-router": { 973 | "version": "0.1.5", 974 | "license": "MIT", 975 | "dependencies": { 976 | "es6-promise": "^3.2.1", 977 | "route-recognizer": "^0.1.11" 978 | } 979 | }, 980 | "node_modules/process": { 981 | "version": "0.11.10", 982 | "license": "MIT", 983 | "engines": { 984 | "node": ">= 0.6.0" 985 | } 986 | }, 987 | "node_modules/proxy-from-env": { 988 | "version": "1.1.0", 989 | "license": "MIT" 990 | }, 991 | "node_modules/route-recognizer": { 992 | "version": "0.1.11", 993 | "license": "MIT" 994 | }, 995 | "node_modules/safe-buffer": { 996 | "version": "5.2.1", 997 | "funding": [ 998 | { 999 | "type": "github", 1000 | "url": "https://github.com/sponsors/feross" 1001 | }, 1002 | { 1003 | "type": "patreon", 1004 | "url": "https://www.patreon.com/feross" 1005 | }, 1006 | { 1007 | "type": "consulting", 1008 | "url": "https://feross.org/support" 1009 | } 1010 | ], 1011 | "license": "MIT" 1012 | }, 1013 | "node_modules/semver": { 1014 | "version": "7.6.2", 1015 | "license": "ISC", 1016 | "bin": { 1017 | "semver": "bin/semver.js" 1018 | }, 1019 | "engines": { 1020 | "node": ">=10" 1021 | } 1022 | }, 1023 | "node_modules/stoppable": { 1024 | "version": "1.1.0", 1025 | "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 1026 | "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 1027 | "dev": true, 1028 | "license": "MIT", 1029 | "engines": { 1030 | "node": ">=4", 1031 | "npm": ">=6" 1032 | } 1033 | }, 1034 | "node_modules/strnum": { 1035 | "version": "1.0.5", 1036 | "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", 1037 | "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", 1038 | "dev": true, 1039 | "license": "MIT" 1040 | }, 1041 | "node_modules/tslib": { 1042 | "version": "2.8.1", 1043 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 1044 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 1045 | "dev": true, 1046 | "license": "0BSD" 1047 | }, 1048 | "node_modules/undici-types": { 1049 | "version": "5.26.5", 1050 | "license": "MIT" 1051 | }, 1052 | "node_modules/util": { 1053 | "version": "0.10.4", 1054 | "license": "MIT", 1055 | "dependencies": { 1056 | "inherits": "2.0.3" 1057 | } 1058 | }, 1059 | "node_modules/uuid": { 1060 | "version": "8.3.2", 1061 | "license": "MIT", 1062 | "bin": { 1063 | "uuid": "dist/bin/uuid" 1064 | } 1065 | }, 1066 | "node_modules/window-post-message-proxy": { 1067 | "version": "0.2.8", 1068 | "license": "MIT", 1069 | "dependencies": { 1070 | "es6-promise": "^3.1.2" 1071 | } 1072 | } 1073 | } 1074 | } 1075 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pbi-dataops-visual-error-testing", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@azure/msal-node": "^2.9.0", 8 | "asynckit": "^0.4.0", 9 | "axios": "^1.7.2", 10 | "buffer-equal-constant-time": "^1.0.1", 11 | "combined-stream": "^1.0.8", 12 | "csv-parse": "^5.5.6", 13 | "delayed-stream": "^1.0.0", 14 | "dotenv": "^16.4.5", 15 | "ecdsa-sig-formatter": "^1.0.11", 16 | "es6-promise": "^3.3.1", 17 | "follow-redirects": "^1.15.6", 18 | "form-data": "^4.0.0", 19 | "fs": "^0.0.1-security", 20 | "http-post-message": "^0.2.3", 21 | "inherits": "^2.0.3", 22 | "jsonwebtoken": "^9.0.2", 23 | "jwa": "^1.4.1", 24 | "jws": "^3.2.2", 25 | "lodash.includes": "^4.3.0", 26 | "lodash.isboolean": "^3.0.3", 27 | "lodash.isinteger": "^4.0.4", 28 | "lodash.isnumber": "^3.0.3", 29 | "lodash.isplainobject": "^4.0.6", 30 | "lodash.isstring": "^4.0.1", 31 | "lodash.once": "^4.1.1", 32 | "mime-db": "^1.52.0", 33 | "mime-types": "^2.1.35", 34 | "ms": "^2.1.3", 35 | "path": "^0.12.7", 36 | "playwright": "^1.47.2", 37 | "playwright-core": "^1.44.1", 38 | "powerbi-client": "^2.23.1", 39 | "powerbi-models": "^1.15.2", 40 | "powerbi-router": "^0.1.5", 41 | "process": "^0.11.10", 42 | "proxy-from-env": "^1.1.0", 43 | "route-recognizer": "^0.1.11", 44 | "safe-buffer": "^5.2.1", 45 | "semver": "^7.6.2", 46 | "undici-types": "^5.26.5", 47 | "util": "^0.10.4", 48 | "uuid": "^8.3.2", 49 | "window-post-message-proxy": "^0.2.8" 50 | }, 51 | "devDependencies": { 52 | "@azure/microsoft-playwright-testing": "^1.0.0-beta.7", 53 | "@playwright/test": "^1.48.2", 54 | "@types/node": "^20.13.0" 55 | }, 56 | "keywords": [], 57 | "author": "", 58 | "license": "ISC" 59 | } 60 | -------------------------------------------------------------------------------- /pipeline-scripts/playwright-automation.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | name: Default 3 | 4 | variables: 5 | - group: VisualTests 6 | 7 | trigger: none 8 | 9 | resources: 10 | pipelines: 11 | - pipeline: 'ci-pipeline' 12 | source: '{XYZ}' 13 | project: ABC 14 | trigger: true 15 | 16 | jobs: 17 | - job: Triggered_Build 18 | workspace: 19 | clean: all 20 | steps: 21 | - download: 'ci-pipeline' 22 | artifact: csvArtifacts 23 | - checkout: self 24 | - task: NodeTool@0 25 | displayName: 'Use Node version 16' 26 | inputs: 27 | versionSpec: 16.x 28 | - script: | 29 | npm ci 30 | displayName: "NPM Install" 31 | - script: | 32 | npx playwright install --with-deps chromium 33 | displayName: "Playwright Install" 34 | - script: | 35 | set CI=true 36 | set PLAYWRIGHT_JUNIT_OUTPUT_NAME=results.xml 37 | npx playwright test 38 | displayName: "Run Playwright Tests" 39 | continueOnError: true 40 | env: 41 | CLIENT_SECRET: $(CLIENT_SECRET) 42 | - task: ArchiveFiles@2 43 | displayName: 'Add playwright-report to Archive' 44 | inputs: 45 | rootFolderOrFile: '$(Pipeline.Workspace)/s/playwright-report/' 46 | archiveFile: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip' 47 | - task: ArchiveFiles@2 48 | displayName: 'Add test-results to Archive' 49 | inputs: 50 | rootFolderOrFile: '$(Pipeline.Workspace)/s/test-results/' 51 | archiveFile: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip' 52 | replaceExistingArchive: false 53 | - task: PublishBuildArtifacts@1 54 | displayName: 'Publish Pipeline Artifacts' 55 | inputs: 56 | PathtoPublish: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip' 57 | ArtifactName: pipeline-artifacts 58 | PublishLocation: Container 59 | - task: PublishTestResults@2 60 | inputs: 61 | testResultsFormat: 'JUnit' 62 | testResultsFiles: '$(Pipeline.Workspace)/s/test-results/results.xml' 63 | failTaskOnFailedTests: true 64 | testRunTitle: 'Playwright Testing Results' 65 | displayName: 'Publish Test Results' -------------------------------------------------------------------------------- /pipeline-scripts/playwright-docker-template.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | - group: VisualTests 3 | 4 | trigger: none # No CI triggers 5 | 6 | schedules: 7 | - cron: "0 */6 * * *" 8 | displayName: "Run every 6 hours" 9 | always: true # Ensures the pipeline runs even if there are no code changes 10 | branches: 11 | include: 12 | - main # Replace with your branch name if different 13 | 14 | jobs: 15 | - job : Run_Playwright_Tests 16 | displayName: 'Run Playwright Tests' 17 | pool: 18 | vmImage: ubuntu-latest 19 | container: mcr.microsoft.com/playwright:v1.48.2-noble 20 | steps: 21 | - task: NodeTool@0 22 | inputs: 23 | versionSpec: '18' 24 | displayName: 'Install Node.js' 25 | - script: npm ci 26 | displayName: 'npm ci' 27 | - script: xvfb-run npx playwright test --config=playwright.config.v1.ts 28 | displayName: "Run Playwright Tests" 29 | continueOnError: true 30 | env: 31 | CLIENT_SECRET: $(CLIENT_SECRET) 32 | - task: PublishTestResults@2 33 | displayName: 'Publish test results' 34 | inputs: 35 | searchFolder: 'test-results' 36 | testResultsFormat: 'JUnit' 37 | testResultsFiles: 'results.xml' 38 | mergeTestResults: false 39 | failTaskOnFailedTests: true 40 | testRunTitle: 'Power BI Tests' 41 | condition: succeededOrFailed() 42 | -------------------------------------------------------------------------------- /pipeline-scripts/playwright-service-template.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | - group: Playwright_Testing_Services 3 | 4 | trigger: none 5 | 6 | jobs: 7 | - job : Run_Playwright_Tests 8 | displayName: 'Run Playwright Tests' 9 | pool: 10 | vmImage: ubuntu-latest 11 | container: mcr.microsoft.com/playwright:v1.48.2-noble 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '18' 16 | displayName: 'Install Node.js' 17 | - script: npm ci 18 | displayName: 'npm ci' 19 | - script: xvfb-run npx playwright test --config=playwright.service.config.ts 20 | displayName: "Run Playwright Tests" 21 | continueOnError: true 22 | env: 23 | CLIENT_SECRET: $(CLIENT_SECRET) 24 | PLAYWRIGHT_SERVICE_ACCESS_TOKEN: $(PLAYWRIGHT_SERVICE_ACCESS_TOKEN) 25 | - task: PublishTestResults@2 26 | displayName: 'Publish test results' 27 | inputs: 28 | searchFolder: 'test-results' 29 | testResultsFormat: 'JUnit' 30 | testResultsFiles: 'results.xml' 31 | mergeTestResults: false 32 | failTaskOnFailedTests: true 33 | testRunTitle: 'Power BI Tests' 34 | condition: succeededOrFailed() -------------------------------------------------------------------------------- /pipeline-scripts/playwright-version1-template.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | vmimage: 'windows-latest' 3 | 4 | variables: 5 | - group: VisualTests 6 | 7 | trigger: none 8 | 9 | jobs: 10 | - job: test 11 | displayName: Run Playwright Tests 12 | steps: 13 | - download: none 14 | - checkout: self 15 | - task: NodeTool@0 16 | displayName: 'Use Node version 16' 17 | inputs: 18 | versionSpec: 16.x 19 | - script: | 20 | npm ci 21 | displayName: "NPM Install" 22 | - script: | 23 | npx playwright install --with-deps chromium 24 | displayName: "Playwright Install" 25 | - script: | 26 | set PLAYWRIGHT_JUNIT_OUTPUT_NAME=results.xml 27 | npx playwright test --config=playwright.config.v1.ts 28 | displayName: "Run Playwright Tests" 29 | continueOnError: true 30 | env: 31 | CLIENT_SECRET: $(CLIENT_SECRET) 32 | - task: ArchiveFiles@2 33 | displayName: 'Add playwright-report to Archive' 34 | inputs: 35 | rootFolderOrFile: '$(Pipeline.Workspace)/s/playwright-report/' 36 | archiveFile: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip' 37 | - task: ArchiveFiles@2 38 | displayName: 'Add test-results to Archive' 39 | inputs: 40 | rootFolderOrFile: '$(Pipeline.Workspace)/s/test-results/' 41 | archiveFile: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip' 42 | replaceExistingArchive: false 43 | - task: PublishPipelineArtifact@1 44 | displayName: 'Publish Pipeline Artifacts' 45 | inputs: 46 | targetPath: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip' 47 | artifact: pipeline-artifacts 48 | - task: PublishTestResults@2 49 | inputs: 50 | testResultsFormat: 'JUnit' 51 | testResultsFiles: '$(Pipeline.Workspace)/s/test-results/results.xml' 52 | testRunTitle: 'Playwright ADO Demo - $(System.StageName)' 53 | displayName: 'Publish Test Results' -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | import dotenv from 'dotenv'; 3 | /** 4 | * See https://playwright.dev/docs/test-configuration. 5 | */ 6 | 7 | // Load .env file 8 | dotenv.config(); 9 | 10 | export default defineConfig({ 11 | timeout: 2 * 60 * 1000, 12 | fullyParallel: true, 13 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 14 | forbidOnly: !!process.env.CI, 15 | /* Retry */ 16 | retries: 0, 17 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 18 | /* Thanks to link for config settings: https://ultimateqa.com/playwright-reporters-how-to-integrate-with-azure-devops-pipelines/*/ 19 | reporter: [['html', {open: 'never'}],['junit', { outputFile: 'test-results/results.xml' }]], 20 | use: { 21 | /* Maximum time each action such as `click()` can take. 22 | Defaults to 0 (no limit). */ 23 | actionTimeout: 60 * 1000, 24 | navigationTimeout: 60 * 1000, 25 | 26 | /* Collect trace when retrying the failed test. 27 | See https://playwright.dev/docs/trace-viewer */ 28 | trace: 'on', 29 | screenshot: 'only-on-failure', 30 | video: { 31 | mode: 'on' 32 | }, 33 | headless: true 34 | }, 35 | globalSetup: require.resolve('./global-setup') 36 | }); 37 | -------------------------------------------------------------------------------- /playwright.config.v1.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | import dotenv from 'dotenv'; 3 | /** 4 | * See https://playwright.dev/docs/test-configuration. 5 | */ 6 | 7 | // Load .env file 8 | dotenv.config(); 9 | 10 | export default defineConfig({ 11 | timeout: 2 * 60 * 1000, 12 | workers: 2, 13 | fullyParallel: true, 14 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 15 | forbidOnly: !!process.env.CI, 16 | /* Retry */ 17 | retries: 1, 18 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 19 | /* Thanks to link for config settings: https://ultimateqa.com/playwright-reporters-how-to-integrate-with-azure-devops-pipelines/*/ 20 | reporter: [['junit', { outputFile: 'test-results/results.xml' }]], 21 | use: { 22 | /* Maximum time each action such as `click()` can take. 23 | Defaults to 0 (no limit). */ 24 | actionTimeout: 60 * 1000, 25 | navigationTimeout: 60 * 1000, 26 | /* Collect trace when retrying the failed test. 27 | See https://playwright.dev/docs/trace-viewer */ 28 | trace: 'on', 29 | screenshot: 'only-on-failure', 30 | video: { 31 | mode: 'on' 32 | }, 33 | headless: true 34 | }, 35 | globalSetup: require.resolve('./global-setup') 36 | }); 37 | -------------------------------------------------------------------------------- /playwright.service.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | import { getServiceConfig, ServiceOS } from '@azure/microsoft-playwright-testing'; 3 | import config from './playwright.config'; 4 | 5 | /* Learn more about service configuration at https://aka.ms/mpt/config */ 6 | export default defineConfig( 7 | config, 8 | getServiceConfig(config, { 9 | exposeNetwork: '', 10 | timeout: 30000, 11 | os: ServiceOS.LINUX, 12 | serviceAuthType:'ACCESS_TOKEN', 13 | useCloudHostedBrowsers: true // Set to false if you want to only use reporting and not cloud hosted browsers 14 | }), 15 | { 16 | /* 17 | Playwright Testing service reporter is added by default. 18 | This will override any reporter options specified in the base playwright config. 19 | If you are using more reporters, please update your configuration accordingly. 20 | */ 21 | reporter: [['list'], ['@azure/microsoft-playwright-testing/reporter'],['junit', { outputFile: 'test-results/results.xml' }]], 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /template.env: -------------------------------------------------------------------------------- 1 | ENV=dev 2 | CI=false 3 | CLIENT_ID="" 4 | CLIENT_SECRET="" 5 | TENANT_ID="" 6 | ENVIRONMENT="Public" 7 | EFFECTIVE_USERNAME = "" -------------------------------------------------------------------------------- /test-cases/placeholder.txt: -------------------------------------------------------------------------------- 1 | This is used to make sure the test-cases folder exists on Git Import. -------------------------------------------------------------------------------- /test-generation/Execute-XMLAQuery.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$ClientId, 3 | [string]$ClientSecret, 4 | [string]$TenantId, 5 | [string]$XmlaQuery, 6 | [string]$DataSource, 7 | [string]$DatasetName, 8 | [string]$AccessToken 9 | ) 10 | 11 | #Install Powershell Module if Needed 12 | if (Get-Module -ListAvailable -Name "SqlServer") { 13 | # Do Nothing 14 | } else { 15 | Install-Module -Name SqlServer -Scope CurrentUser -AllowClobber -Force 16 | } 17 | 18 | # Setup the connection string 19 | $secret = $ClientSecret | ConvertTo-SecureString -AsPlainText -Force 20 | $credentials = [System.Management.Automation.PSCredential]::new($ClientId,$secret) 21 | 22 | try { 23 | $Result = Invoke-ASCmd -Server $DataSource ` 24 | -Database $DatasetName ` 25 | -Query $XmlaQuery ` 26 | -Credential $credentials ` 27 | -TenantId $TenantId -ServicePrincipal 28 | 29 | #Remove unicode chars for brackets and spaces from XML node names 30 | $Result = $Result -replace '_x[0-9A-z]{4}_', ''; 31 | 32 | $Result 33 | 34 | } catch { 35 | Write-Host "Error: $_" 36 | } 37 | -------------------------------------------------------------------------------- /test-generation/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const { exec } = require('child_process'); 4 | const xml2js = require('xml2js'); 5 | const { ConfidentialClientApplication } = require('@azure/msal-node'); 6 | const { get } = require('http'); 7 | const app = express(); 8 | const port = 3000; 9 | // Load environment variables from .env file 10 | require('dotenv').config({ path: path.resolve(__dirname, '../.env') }); 11 | // Use environment variables from .env file 12 | const tenantId = process.env.TENANT_ID; 13 | const clientId = process.env.CLIENT_ID; 14 | const clientSecret = process.env.CLIENT_SECRET; 15 | const environment = process.env.ENVIRONMENT; 16 | const testSettings = getAPIEndpoints(environment); 17 | // Handle file uploads 18 | const multer = require('multer'); 19 | const fs = require('fs'); 20 | 21 | const msalConfig = { 22 | auth: { 23 | clientId: clientId, 24 | authority: `${testSettings.loginUrl}/${tenantId}`, 25 | clientSecret: clientSecret 26 | } 27 | }; 28 | const cca = new ConfidentialClientApplication(msalConfig); 29 | // Define the scopes for the access token 30 | const tokenRequest = { scopes: [`${testSettings.resourceUrl}/.default`] }; 31 | 32 | // Function to get API endpoints based on the environment 33 | function getAPIEndpoints(environment) { 34 | // Default endpoints 35 | let endpoints = { 36 | apiPrefix: 'https://api.powerbi.com', 37 | xmlaPrefix: 'powerbi://api.powerbi.com', 38 | webPrefix: 'https://app.powerbi.com', 39 | resourceUrl: 'https://analysis.windows.net/powerbi/api', 40 | embedUrl: 'https://app.powerbi.com/reportEmbed', 41 | loginUrl: 'https://login.microsoftonline.com' 42 | }; 43 | 44 | // Switch case to set endpoints based on the environment 45 | switch (environment) { 46 | case "Public": 47 | break; 48 | case "Germany": 49 | endpoints.apiPrefix = "https://api.powerbi.de"; 50 | endpoints.xmlaPrefix = "powerbi://api.powerbi.de"; 51 | endpoints.webPrefix = "https://app.powerbi.de"; 52 | endpoints.resourceUrl = "https://analysis.cloudapi.de/powerbi/api"; 53 | endpoints.embedUrl = "https://app.powerbi.de/reportEmbed"; 54 | endpoints.loginUrl = "https://login.microsoftonline.com"; 55 | break; 56 | case "China": 57 | endpoints.apiPrefix = "https://api.powerbi.cn"; 58 | endpoints.xmlaPrefix = "powerbi://api.powerbi.cn"; 59 | endpoints.webPrefix = "https://app.powerbigov.cn"; 60 | endpoints.resourceUrl = "https://analysis.chinacloudapi.cn/powerbi/api"; 61 | endpoints.embedUrl = "https://app.powerbi.cn/reportEmbed"; 62 | endpoints.loginUrl = "https://login.partner.microsoftonline.cn"; 63 | break; 64 | case "USGov": 65 | endpoints.apiPrefix = "https://api.powerbigov.us"; 66 | endpoints.xmlaPrefix = "powerbi://api.powerbigov.us"; 67 | endpoints.webPrefix = "https://app.powerbigov.us"; 68 | endpoints.resourceUrl = "https://analysis.usgovcloudapi.net/powerbi/api"; 69 | endpoints.embedUrl = "https://app.powerbigov.us/reportEmbed"; 70 | endpoints.loginUrl = "https://login.microsoftonline.com"; 71 | break; 72 | case "USGovHigh": 73 | endpoints.apiPrefix = "https://api.high.powerbigov.us"; 74 | endpoints.xmlaPrefix = "powerbi://api.high.powerbigov.us"; 75 | endpoints.webPrefix = "https://app.high.powerbigov.us"; 76 | endpoints.resourceUrl = "https://analysis.high.usgovcloudapi.net/powerbi/api"; 77 | endpoints.embedUrl = "https://app.high.powerbigov.us/reportEmbed"; 78 | endpoints.loginUrl = "https://login.microsoftonline.us"; 79 | break; 80 | case "USGovDoD": 81 | endpoints.apiPrefix = "https://api.mil.powerbi.us"; 82 | endpoints.xmlaPrefix = "powerbi://api.mil.powerbi.us"; 83 | endpoints.webPrefix = "https://app.mil.powerbi.us"; 84 | endpoints.resourceUrl = "https://analysis.dod.usgovcloudapi.net/powerbi/api"; 85 | endpoints.embedUrl = "https://app.mil.powerbi.us/reportEmbed"; 86 | endpoints.loginUrl = "https://login.microsoftonline.us"; 87 | break; 88 | default: 89 | break; 90 | } 91 | 92 | // Return the endpoints 93 | return endpoints; 94 | } 95 | 96 | // Create the 'test-cases' directory if it doesn't exist 97 | const testCasesDir = path.join(__dirname, '../test-cases'); 98 | if (!fs.existsSync(testCasesDir)) { 99 | fs.mkdirSync(testCasesDir, { recursive: true }); // Ensure the directory is created 100 | } 101 | 102 | // Configure multer for file uploads 103 | const storage = multer.diskStorage({ 104 | destination: function (req, file, cb) { 105 | cb(null, testCasesDir); // Save to 'test-cases' folder 106 | }, 107 | filename: function (req, file, cb) { 108 | cb(null, file.originalname); // Use the original file name 109 | } 110 | }); 111 | 112 | const upload = multer({ storage: storage }); 113 | 114 | 115 | // ENDPOINTS 116 | 117 | // Get Access Token 118 | async function getAccessToken() { 119 | const apiPrefix = getAPIEndpoints(process.env.ENVIRONMENT).apiPrefix; 120 | const tokenResponse = await cca.acquireTokenByClientCredential(tokenRequest); 121 | return { accessToken: tokenResponse.accessToken, apiUrl: apiPrefix, effectiveUserName: process.env.EFFECTIVE_USERNAME } 122 | } 123 | 124 | // Route to get access token 125 | app.get('/getToken', async (req, res) => { 126 | try { 127 | const response = await getAccessToken(); 128 | res.json(response); 129 | } catch (error) { 130 | console.error('Error acquiring token:', error); 131 | res.status(500).json({ error: 'Failed to get access token' }); 132 | } 133 | }); 134 | 135 | // Route to execute PowerShell script 136 | app.get('/executeScript', async (req, res) => { 137 | const workspaceName = req.query.workspaceName; 138 | const datasetName = req.query.datasetName; 139 | 140 | if (!workspaceName || !datasetName) { 141 | return res.status(400).json({ error: "Workspace Name and Dataset Name are required" }); 142 | } 143 | 144 | try { 145 | // Get access token 146 | const { accessToken } = await getAccessToken(); 147 | 148 | // Path to your PowerShell script 149 | const scriptPath = path.join(__dirname, 'Execute-XMLAQuery.ps1'); 150 | 151 | // Define the dataset name and XMLA query based on the report 152 | const xmlaQuery = "EVALUATE INFO.ROLES()"; 153 | const dataSource = `${testSettings.xmlaPrefix}/v1.0/myorg/${workspaceName}`; 154 | 155 | // Prepare the PowerShell command 156 | const command = `pwsh.exe -ExecutionPolicy Bypass -File "${scriptPath}" -ClientId "${clientId}" -ClientSecret "${clientSecret}" -TenantId "${tenantId}" -XmlaQuery "${xmlaQuery}" -DataSource "${dataSource}" -DatasetName "${datasetName}" -AccessToken "${accessToken}"`; 157 | 158 | // Execute the PowerShell script 159 | exec(command, (error, stdout, stderr) => { 160 | if (error) { 161 | console.error(`Error executing PowerShell script: ${error.message}`); 162 | res.status(500).json({ error: 'Failed to execute script' }); 163 | return; 164 | } 165 | if (stderr) { 166 | console.error(`PowerShell script error: ${stderr}`); 167 | res.status(500).json({ error: 'PowerShell error' }); 168 | return; 169 | } 170 | 171 | console.log("Query Results:", stdout); 172 | 173 | // Parse the XMLA response 174 | const parser = new xml2js.Parser(); 175 | parser.parseString(stdout, (err, result) => { 176 | if (err) { 177 | return res.status(500).send("Error parsing XML"); 178 | } 179 | 180 | // Navigate to the rows in the parsed XML, check if rows exist 181 | const rows = result?.['return']?.['root']?.[0]?.['row']; 182 | 183 | if (!rows || rows.length === 0) { 184 | return res.json({ message: "No rows found" }); 185 | } 186 | 187 | // Extract the values for the C2 nodes, checking if the node exists 188 | const roles = rows.map(row => { 189 | // Check if C2 exists and is an array with at least one value 190 | return row['C2'] && row['C2'].length > 0 ? row['C2'][0] : 'C2 node not found'; 191 | }); 192 | 193 | res.json({ roles }); 194 | }) 195 | }); 196 | 197 | } catch (error) { 198 | console.error('Error executing script:', error); 199 | res.status(500).json({ error: 'Failed to execute script' }); 200 | } 201 | }); 202 | 203 | // Serve index.html 204 | app.get('/', (req, res) => { 205 | res.sendFile(path.join(__dirname, 'views', 'index.html')); 206 | }); 207 | 208 | // Route to upload a CSV test file 209 | app.post('/uploadCsv', upload.single('testFile'), (req, res) => { 210 | // Check if file was uploaded 211 | if (!req.file) { 212 | return res.status(400).json({ error: 'No file uploaded' }); 213 | } 214 | 215 | res.json({ message: 'File uploaded successfully', fileName: req.file.originalname }); 216 | }); 217 | 218 | // Route to upload a JSON test file 219 | app.post('/uploadJson', upload.single('testFile'), (req, res) => { 220 | // Check if file was uploaded 221 | if (!req.file) { 222 | return res.status(400).json({ error: 'No file uploaded' }); 223 | } 224 | 225 | res.json({ message: 'File uploaded successfully', fileName: req.file.originalname }); 226 | }); 227 | 228 | app.listen(port, () => { 229 | console.log(`App running at http://localhost:${port}`); 230 | }); 231 | -------------------------------------------------------------------------------- /test-generation/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-generation", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "test-generation", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@azure/msal-node": "^2.15.0", 13 | "axios": "^1.7.7", 14 | "child_process": "^1.0.2", 15 | "dotenv": "^16.4.5", 16 | "edge": "^7.10.1", 17 | "express": "^4.21.1", 18 | "fs": "^0.0.1-security", 19 | "multer": "^1.4.5-lts.1", 20 | "xml2js": "^0.6.2" 21 | } 22 | }, 23 | "node_modules/@azure/msal-common": { 24 | "version": "14.15.0", 25 | "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", 26 | "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", 27 | "engines": { 28 | "node": ">=0.8.0" 29 | } 30 | }, 31 | "node_modules/@azure/msal-node": { 32 | "version": "2.15.0", 33 | "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.15.0.tgz", 34 | "integrity": "sha512-gVPW8YLz92ZeCibQH2QUw96odJoiM3k/ZPH3f2HxptozmH6+OnyyvKXo/Egg39HAM230akarQKHf0W74UHlh0Q==", 35 | "dependencies": { 36 | "@azure/msal-common": "14.15.0", 37 | "jsonwebtoken": "^9.0.0", 38 | "uuid": "^8.3.0" 39 | }, 40 | "engines": { 41 | "node": ">=16" 42 | } 43 | }, 44 | "node_modules/accepts": { 45 | "version": "1.3.8", 46 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 47 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 48 | "dependencies": { 49 | "mime-types": "~2.1.34", 50 | "negotiator": "0.6.3" 51 | }, 52 | "engines": { 53 | "node": ">= 0.6" 54 | } 55 | }, 56 | "node_modules/append-field": { 57 | "version": "1.0.0", 58 | "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", 59 | "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" 60 | }, 61 | "node_modules/array-flatten": { 62 | "version": "1.1.1", 63 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 64 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 65 | }, 66 | "node_modules/asynckit": { 67 | "version": "0.4.0", 68 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 69 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 70 | }, 71 | "node_modules/axios": { 72 | "version": "1.8.4", 73 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", 74 | "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", 75 | "license": "MIT", 76 | "dependencies": { 77 | "follow-redirects": "^1.15.6", 78 | "form-data": "^4.0.0", 79 | "proxy-from-env": "^1.1.0" 80 | } 81 | }, 82 | "node_modules/body-parser": { 83 | "version": "1.20.3", 84 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 85 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 86 | "dependencies": { 87 | "bytes": "3.1.2", 88 | "content-type": "~1.0.5", 89 | "debug": "2.6.9", 90 | "depd": "2.0.0", 91 | "destroy": "1.2.0", 92 | "http-errors": "2.0.0", 93 | "iconv-lite": "0.4.24", 94 | "on-finished": "2.4.1", 95 | "qs": "6.13.0", 96 | "raw-body": "2.5.2", 97 | "type-is": "~1.6.18", 98 | "unpipe": "1.0.0" 99 | }, 100 | "engines": { 101 | "node": ">= 0.8", 102 | "npm": "1.2.8000 || >= 1.4.16" 103 | } 104 | }, 105 | "node_modules/buffer-equal-constant-time": { 106 | "version": "1.0.1", 107 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 108 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 109 | }, 110 | "node_modules/buffer-from": { 111 | "version": "1.1.2", 112 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 113 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" 114 | }, 115 | "node_modules/busboy": { 116 | "version": "1.6.0", 117 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 118 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 119 | "dependencies": { 120 | "streamsearch": "^1.1.0" 121 | }, 122 | "engines": { 123 | "node": ">=10.16.0" 124 | } 125 | }, 126 | "node_modules/bytes": { 127 | "version": "3.1.2", 128 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 129 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 130 | "engines": { 131 | "node": ">= 0.8" 132 | } 133 | }, 134 | "node_modules/call-bind": { 135 | "version": "1.0.7", 136 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 137 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 138 | "dependencies": { 139 | "es-define-property": "^1.0.0", 140 | "es-errors": "^1.3.0", 141 | "function-bind": "^1.1.2", 142 | "get-intrinsic": "^1.2.4", 143 | "set-function-length": "^1.2.1" 144 | }, 145 | "engines": { 146 | "node": ">= 0.4" 147 | }, 148 | "funding": { 149 | "url": "https://github.com/sponsors/ljharb" 150 | } 151 | }, 152 | "node_modules/call-bind-apply-helpers": { 153 | "version": "1.0.2", 154 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 155 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 156 | "license": "MIT", 157 | "dependencies": { 158 | "es-errors": "^1.3.0", 159 | "function-bind": "^1.1.2" 160 | }, 161 | "engines": { 162 | "node": ">= 0.4" 163 | } 164 | }, 165 | "node_modules/child_process": { 166 | "version": "1.0.2", 167 | "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", 168 | "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" 169 | }, 170 | "node_modules/combined-stream": { 171 | "version": "1.0.8", 172 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 173 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 174 | "dependencies": { 175 | "delayed-stream": "~1.0.0" 176 | }, 177 | "engines": { 178 | "node": ">= 0.8" 179 | } 180 | }, 181 | "node_modules/concat-stream": { 182 | "version": "1.6.2", 183 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 184 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 185 | "engines": [ 186 | "node >= 0.8" 187 | ], 188 | "dependencies": { 189 | "buffer-from": "^1.0.0", 190 | "inherits": "^2.0.3", 191 | "readable-stream": "^2.2.2", 192 | "typedarray": "^0.0.6" 193 | } 194 | }, 195 | "node_modules/content-disposition": { 196 | "version": "0.5.4", 197 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 198 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 199 | "dependencies": { 200 | "safe-buffer": "5.2.1" 201 | }, 202 | "engines": { 203 | "node": ">= 0.6" 204 | } 205 | }, 206 | "node_modules/content-type": { 207 | "version": "1.0.5", 208 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 209 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 210 | "engines": { 211 | "node": ">= 0.6" 212 | } 213 | }, 214 | "node_modules/cookie": { 215 | "version": "0.7.1", 216 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 217 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 218 | "engines": { 219 | "node": ">= 0.6" 220 | } 221 | }, 222 | "node_modules/cookie-signature": { 223 | "version": "1.0.6", 224 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 225 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 226 | }, 227 | "node_modules/core-util-is": { 228 | "version": "1.0.3", 229 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 230 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 231 | }, 232 | "node_modules/debug": { 233 | "version": "2.6.9", 234 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 235 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 236 | "dependencies": { 237 | "ms": "2.0.0" 238 | } 239 | }, 240 | "node_modules/define-data-property": { 241 | "version": "1.1.4", 242 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 243 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 244 | "dependencies": { 245 | "es-define-property": "^1.0.0", 246 | "es-errors": "^1.3.0", 247 | "gopd": "^1.0.1" 248 | }, 249 | "engines": { 250 | "node": ">= 0.4" 251 | }, 252 | "funding": { 253 | "url": "https://github.com/sponsors/ljharb" 254 | } 255 | }, 256 | "node_modules/delayed-stream": { 257 | "version": "1.0.0", 258 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 259 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 260 | "engines": { 261 | "node": ">=0.4.0" 262 | } 263 | }, 264 | "node_modules/depd": { 265 | "version": "2.0.0", 266 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 267 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 268 | "engines": { 269 | "node": ">= 0.8" 270 | } 271 | }, 272 | "node_modules/destroy": { 273 | "version": "1.2.0", 274 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 275 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 276 | "engines": { 277 | "node": ">= 0.8", 278 | "npm": "1.2.8000 || >= 1.4.16" 279 | } 280 | }, 281 | "node_modules/dotenv": { 282 | "version": "16.4.5", 283 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 284 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 285 | "engines": { 286 | "node": ">=12" 287 | }, 288 | "funding": { 289 | "url": "https://dotenvx.com" 290 | } 291 | }, 292 | "node_modules/dunder-proto": { 293 | "version": "1.0.1", 294 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 295 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 296 | "license": "MIT", 297 | "dependencies": { 298 | "call-bind-apply-helpers": "^1.0.1", 299 | "es-errors": "^1.3.0", 300 | "gopd": "^1.2.0" 301 | }, 302 | "engines": { 303 | "node": ">= 0.4" 304 | } 305 | }, 306 | "node_modules/ecdsa-sig-formatter": { 307 | "version": "1.0.11", 308 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 309 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 310 | "dependencies": { 311 | "safe-buffer": "^5.0.1" 312 | } 313 | }, 314 | "node_modules/edge": { 315 | "version": "7.10.1", 316 | "resolved": "https://registry.npmjs.org/edge/-/edge-7.10.1.tgz", 317 | "integrity": "sha512-3rTG+z6houHRamNK7aTqm6rQnFi19H5yYGh0lXNBS5RlP6vQIGlBBwELEaUBR+h9WxSb225AUis4afiDg2w3yw==", 318 | "hasInstallScript": true, 319 | "dependencies": { 320 | "edge-cs": "1.2.1", 321 | "nan": "^2.0.9" 322 | }, 323 | "engines": { 324 | "node": ">= 0.8" 325 | } 326 | }, 327 | "node_modules/edge-cs": { 328 | "version": "1.2.1", 329 | "resolved": "https://registry.npmjs.org/edge-cs/-/edge-cs-1.2.1.tgz", 330 | "integrity": "sha512-N5KAeMBhhCbCGmb5oiHJ0KcuTksZzo8lg+uByEUCZAdnLPHizHkk6ZIuuTD63eez7+W25ZSvsGS5+FtymxFoKw==", 331 | "hasInstallScript": true, 332 | "engines": { 333 | "node": ">= 0.8" 334 | } 335 | }, 336 | "node_modules/ee-first": { 337 | "version": "1.1.1", 338 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 339 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 340 | }, 341 | "node_modules/encodeurl": { 342 | "version": "2.0.0", 343 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 344 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 345 | "engines": { 346 | "node": ">= 0.8" 347 | } 348 | }, 349 | "node_modules/es-define-property": { 350 | "version": "1.0.1", 351 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 352 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 353 | "license": "MIT", 354 | "engines": { 355 | "node": ">= 0.4" 356 | } 357 | }, 358 | "node_modules/es-errors": { 359 | "version": "1.3.0", 360 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 361 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 362 | "engines": { 363 | "node": ">= 0.4" 364 | } 365 | }, 366 | "node_modules/es-object-atoms": { 367 | "version": "1.1.1", 368 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 369 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 370 | "license": "MIT", 371 | "dependencies": { 372 | "es-errors": "^1.3.0" 373 | }, 374 | "engines": { 375 | "node": ">= 0.4" 376 | } 377 | }, 378 | "node_modules/es-set-tostringtag": { 379 | "version": "2.1.0", 380 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 381 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 382 | "license": "MIT", 383 | "dependencies": { 384 | "es-errors": "^1.3.0", 385 | "get-intrinsic": "^1.2.6", 386 | "has-tostringtag": "^1.0.2", 387 | "hasown": "^2.0.2" 388 | }, 389 | "engines": { 390 | "node": ">= 0.4" 391 | } 392 | }, 393 | "node_modules/escape-html": { 394 | "version": "1.0.3", 395 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 396 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 397 | }, 398 | "node_modules/etag": { 399 | "version": "1.8.1", 400 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 401 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 402 | "engines": { 403 | "node": ">= 0.6" 404 | } 405 | }, 406 | "node_modules/express": { 407 | "version": "4.21.2", 408 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", 409 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 410 | "license": "MIT", 411 | "dependencies": { 412 | "accepts": "~1.3.8", 413 | "array-flatten": "1.1.1", 414 | "body-parser": "1.20.3", 415 | "content-disposition": "0.5.4", 416 | "content-type": "~1.0.4", 417 | "cookie": "0.7.1", 418 | "cookie-signature": "1.0.6", 419 | "debug": "2.6.9", 420 | "depd": "2.0.0", 421 | "encodeurl": "~2.0.0", 422 | "escape-html": "~1.0.3", 423 | "etag": "~1.8.1", 424 | "finalhandler": "1.3.1", 425 | "fresh": "0.5.2", 426 | "http-errors": "2.0.0", 427 | "merge-descriptors": "1.0.3", 428 | "methods": "~1.1.2", 429 | "on-finished": "2.4.1", 430 | "parseurl": "~1.3.3", 431 | "path-to-regexp": "0.1.12", 432 | "proxy-addr": "~2.0.7", 433 | "qs": "6.13.0", 434 | "range-parser": "~1.2.1", 435 | "safe-buffer": "5.2.1", 436 | "send": "0.19.0", 437 | "serve-static": "1.16.2", 438 | "setprototypeof": "1.2.0", 439 | "statuses": "2.0.1", 440 | "type-is": "~1.6.18", 441 | "utils-merge": "1.0.1", 442 | "vary": "~1.1.2" 443 | }, 444 | "engines": { 445 | "node": ">= 0.10.0" 446 | }, 447 | "funding": { 448 | "type": "opencollective", 449 | "url": "https://opencollective.com/express" 450 | } 451 | }, 452 | "node_modules/finalhandler": { 453 | "version": "1.3.1", 454 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 455 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 456 | "dependencies": { 457 | "debug": "2.6.9", 458 | "encodeurl": "~2.0.0", 459 | "escape-html": "~1.0.3", 460 | "on-finished": "2.4.1", 461 | "parseurl": "~1.3.3", 462 | "statuses": "2.0.1", 463 | "unpipe": "~1.0.0" 464 | }, 465 | "engines": { 466 | "node": ">= 0.8" 467 | } 468 | }, 469 | "node_modules/follow-redirects": { 470 | "version": "1.15.9", 471 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 472 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 473 | "funding": [ 474 | { 475 | "type": "individual", 476 | "url": "https://github.com/sponsors/RubenVerborgh" 477 | } 478 | ], 479 | "engines": { 480 | "node": ">=4.0" 481 | }, 482 | "peerDependenciesMeta": { 483 | "debug": { 484 | "optional": true 485 | } 486 | } 487 | }, 488 | "node_modules/form-data": { 489 | "version": "4.0.4", 490 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", 491 | "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", 492 | "license": "MIT", 493 | "dependencies": { 494 | "asynckit": "^0.4.0", 495 | "combined-stream": "^1.0.8", 496 | "es-set-tostringtag": "^2.1.0", 497 | "hasown": "^2.0.2", 498 | "mime-types": "^2.1.12" 499 | }, 500 | "engines": { 501 | "node": ">= 6" 502 | } 503 | }, 504 | "node_modules/forwarded": { 505 | "version": "0.2.0", 506 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 507 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 508 | "engines": { 509 | "node": ">= 0.6" 510 | } 511 | }, 512 | "node_modules/fresh": { 513 | "version": "0.5.2", 514 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 515 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 516 | "engines": { 517 | "node": ">= 0.6" 518 | } 519 | }, 520 | "node_modules/fs": { 521 | "version": "0.0.1-security", 522 | "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", 523 | "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" 524 | }, 525 | "node_modules/function-bind": { 526 | "version": "1.1.2", 527 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 528 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 529 | "funding": { 530 | "url": "https://github.com/sponsors/ljharb" 531 | } 532 | }, 533 | "node_modules/get-intrinsic": { 534 | "version": "1.3.0", 535 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 536 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 537 | "license": "MIT", 538 | "dependencies": { 539 | "call-bind-apply-helpers": "^1.0.2", 540 | "es-define-property": "^1.0.1", 541 | "es-errors": "^1.3.0", 542 | "es-object-atoms": "^1.1.1", 543 | "function-bind": "^1.1.2", 544 | "get-proto": "^1.0.1", 545 | "gopd": "^1.2.0", 546 | "has-symbols": "^1.1.0", 547 | "hasown": "^2.0.2", 548 | "math-intrinsics": "^1.1.0" 549 | }, 550 | "engines": { 551 | "node": ">= 0.4" 552 | }, 553 | "funding": { 554 | "url": "https://github.com/sponsors/ljharb" 555 | } 556 | }, 557 | "node_modules/get-proto": { 558 | "version": "1.0.1", 559 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 560 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 561 | "license": "MIT", 562 | "dependencies": { 563 | "dunder-proto": "^1.0.1", 564 | "es-object-atoms": "^1.0.0" 565 | }, 566 | "engines": { 567 | "node": ">= 0.4" 568 | } 569 | }, 570 | "node_modules/gopd": { 571 | "version": "1.2.0", 572 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 573 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 574 | "license": "MIT", 575 | "engines": { 576 | "node": ">= 0.4" 577 | }, 578 | "funding": { 579 | "url": "https://github.com/sponsors/ljharb" 580 | } 581 | }, 582 | "node_modules/has-property-descriptors": { 583 | "version": "1.0.2", 584 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 585 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 586 | "dependencies": { 587 | "es-define-property": "^1.0.0" 588 | }, 589 | "funding": { 590 | "url": "https://github.com/sponsors/ljharb" 591 | } 592 | }, 593 | "node_modules/has-symbols": { 594 | "version": "1.1.0", 595 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 596 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 597 | "license": "MIT", 598 | "engines": { 599 | "node": ">= 0.4" 600 | }, 601 | "funding": { 602 | "url": "https://github.com/sponsors/ljharb" 603 | } 604 | }, 605 | "node_modules/has-tostringtag": { 606 | "version": "1.0.2", 607 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 608 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 609 | "license": "MIT", 610 | "dependencies": { 611 | "has-symbols": "^1.0.3" 612 | }, 613 | "engines": { 614 | "node": ">= 0.4" 615 | }, 616 | "funding": { 617 | "url": "https://github.com/sponsors/ljharb" 618 | } 619 | }, 620 | "node_modules/hasown": { 621 | "version": "2.0.2", 622 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 623 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 624 | "dependencies": { 625 | "function-bind": "^1.1.2" 626 | }, 627 | "engines": { 628 | "node": ">= 0.4" 629 | } 630 | }, 631 | "node_modules/http-errors": { 632 | "version": "2.0.0", 633 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 634 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 635 | "dependencies": { 636 | "depd": "2.0.0", 637 | "inherits": "2.0.4", 638 | "setprototypeof": "1.2.0", 639 | "statuses": "2.0.1", 640 | "toidentifier": "1.0.1" 641 | }, 642 | "engines": { 643 | "node": ">= 0.8" 644 | } 645 | }, 646 | "node_modules/iconv-lite": { 647 | "version": "0.4.24", 648 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 649 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 650 | "dependencies": { 651 | "safer-buffer": ">= 2.1.2 < 3" 652 | }, 653 | "engines": { 654 | "node": ">=0.10.0" 655 | } 656 | }, 657 | "node_modules/inherits": { 658 | "version": "2.0.4", 659 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 660 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 661 | }, 662 | "node_modules/ipaddr.js": { 663 | "version": "1.9.1", 664 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 665 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 666 | "engines": { 667 | "node": ">= 0.10" 668 | } 669 | }, 670 | "node_modules/isarray": { 671 | "version": "1.0.0", 672 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 673 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 674 | }, 675 | "node_modules/jsonwebtoken": { 676 | "version": "9.0.2", 677 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", 678 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 679 | "dependencies": { 680 | "jws": "^3.2.2", 681 | "lodash.includes": "^4.3.0", 682 | "lodash.isboolean": "^3.0.3", 683 | "lodash.isinteger": "^4.0.4", 684 | "lodash.isnumber": "^3.0.3", 685 | "lodash.isplainobject": "^4.0.6", 686 | "lodash.isstring": "^4.0.1", 687 | "lodash.once": "^4.0.0", 688 | "ms": "^2.1.1", 689 | "semver": "^7.5.4" 690 | }, 691 | "engines": { 692 | "node": ">=12", 693 | "npm": ">=6" 694 | } 695 | }, 696 | "node_modules/jsonwebtoken/node_modules/ms": { 697 | "version": "2.1.3", 698 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 699 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 700 | }, 701 | "node_modules/jwa": { 702 | "version": "1.4.1", 703 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 704 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 705 | "dependencies": { 706 | "buffer-equal-constant-time": "1.0.1", 707 | "ecdsa-sig-formatter": "1.0.11", 708 | "safe-buffer": "^5.0.1" 709 | } 710 | }, 711 | "node_modules/jws": { 712 | "version": "3.2.2", 713 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 714 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 715 | "dependencies": { 716 | "jwa": "^1.4.1", 717 | "safe-buffer": "^5.0.1" 718 | } 719 | }, 720 | "node_modules/lodash.includes": { 721 | "version": "4.3.0", 722 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 723 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" 724 | }, 725 | "node_modules/lodash.isboolean": { 726 | "version": "3.0.3", 727 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 728 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" 729 | }, 730 | "node_modules/lodash.isinteger": { 731 | "version": "4.0.4", 732 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 733 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" 734 | }, 735 | "node_modules/lodash.isnumber": { 736 | "version": "3.0.3", 737 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 738 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" 739 | }, 740 | "node_modules/lodash.isplainobject": { 741 | "version": "4.0.6", 742 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 743 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" 744 | }, 745 | "node_modules/lodash.isstring": { 746 | "version": "4.0.1", 747 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 748 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" 749 | }, 750 | "node_modules/lodash.once": { 751 | "version": "4.1.1", 752 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 753 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" 754 | }, 755 | "node_modules/math-intrinsics": { 756 | "version": "1.1.0", 757 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 758 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 759 | "license": "MIT", 760 | "engines": { 761 | "node": ">= 0.4" 762 | } 763 | }, 764 | "node_modules/media-typer": { 765 | "version": "0.3.0", 766 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 767 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 768 | "engines": { 769 | "node": ">= 0.6" 770 | } 771 | }, 772 | "node_modules/merge-descriptors": { 773 | "version": "1.0.3", 774 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 775 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 776 | "funding": { 777 | "url": "https://github.com/sponsors/sindresorhus" 778 | } 779 | }, 780 | "node_modules/methods": { 781 | "version": "1.1.2", 782 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 783 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 784 | "engines": { 785 | "node": ">= 0.6" 786 | } 787 | }, 788 | "node_modules/mime": { 789 | "version": "1.6.0", 790 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 791 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 792 | "bin": { 793 | "mime": "cli.js" 794 | }, 795 | "engines": { 796 | "node": ">=4" 797 | } 798 | }, 799 | "node_modules/mime-db": { 800 | "version": "1.52.0", 801 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 802 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 803 | "engines": { 804 | "node": ">= 0.6" 805 | } 806 | }, 807 | "node_modules/mime-types": { 808 | "version": "2.1.35", 809 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 810 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 811 | "dependencies": { 812 | "mime-db": "1.52.0" 813 | }, 814 | "engines": { 815 | "node": ">= 0.6" 816 | } 817 | }, 818 | "node_modules/minimist": { 819 | "version": "1.2.8", 820 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 821 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 822 | "funding": { 823 | "url": "https://github.com/sponsors/ljharb" 824 | } 825 | }, 826 | "node_modules/mkdirp": { 827 | "version": "0.5.6", 828 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 829 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 830 | "dependencies": { 831 | "minimist": "^1.2.6" 832 | }, 833 | "bin": { 834 | "mkdirp": "bin/cmd.js" 835 | } 836 | }, 837 | "node_modules/ms": { 838 | "version": "2.0.0", 839 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 840 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 841 | }, 842 | "node_modules/multer": { 843 | "version": "1.4.5-lts.1", 844 | "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", 845 | "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", 846 | "dependencies": { 847 | "append-field": "^1.0.0", 848 | "busboy": "^1.0.0", 849 | "concat-stream": "^1.5.2", 850 | "mkdirp": "^0.5.4", 851 | "object-assign": "^4.1.1", 852 | "type-is": "^1.6.4", 853 | "xtend": "^4.0.0" 854 | }, 855 | "engines": { 856 | "node": ">= 6.0.0" 857 | } 858 | }, 859 | "node_modules/nan": { 860 | "version": "2.22.0", 861 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", 862 | "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==" 863 | }, 864 | "node_modules/negotiator": { 865 | "version": "0.6.3", 866 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 867 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 868 | "engines": { 869 | "node": ">= 0.6" 870 | } 871 | }, 872 | "node_modules/object-assign": { 873 | "version": "4.1.1", 874 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 875 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 876 | "engines": { 877 | "node": ">=0.10.0" 878 | } 879 | }, 880 | "node_modules/object-inspect": { 881 | "version": "1.13.2", 882 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", 883 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", 884 | "engines": { 885 | "node": ">= 0.4" 886 | }, 887 | "funding": { 888 | "url": "https://github.com/sponsors/ljharb" 889 | } 890 | }, 891 | "node_modules/on-finished": { 892 | "version": "2.4.1", 893 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 894 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 895 | "dependencies": { 896 | "ee-first": "1.1.1" 897 | }, 898 | "engines": { 899 | "node": ">= 0.8" 900 | } 901 | }, 902 | "node_modules/parseurl": { 903 | "version": "1.3.3", 904 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 905 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 906 | "engines": { 907 | "node": ">= 0.8" 908 | } 909 | }, 910 | "node_modules/path-to-regexp": { 911 | "version": "0.1.12", 912 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 913 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 914 | "license": "MIT" 915 | }, 916 | "node_modules/process-nextick-args": { 917 | "version": "2.0.1", 918 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 919 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 920 | }, 921 | "node_modules/proxy-addr": { 922 | "version": "2.0.7", 923 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 924 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 925 | "dependencies": { 926 | "forwarded": "0.2.0", 927 | "ipaddr.js": "1.9.1" 928 | }, 929 | "engines": { 930 | "node": ">= 0.10" 931 | } 932 | }, 933 | "node_modules/proxy-from-env": { 934 | "version": "1.1.0", 935 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 936 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 937 | }, 938 | "node_modules/qs": { 939 | "version": "6.13.0", 940 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 941 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 942 | "dependencies": { 943 | "side-channel": "^1.0.6" 944 | }, 945 | "engines": { 946 | "node": ">=0.6" 947 | }, 948 | "funding": { 949 | "url": "https://github.com/sponsors/ljharb" 950 | } 951 | }, 952 | "node_modules/range-parser": { 953 | "version": "1.2.1", 954 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 955 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 956 | "engines": { 957 | "node": ">= 0.6" 958 | } 959 | }, 960 | "node_modules/raw-body": { 961 | "version": "2.5.2", 962 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 963 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 964 | "dependencies": { 965 | "bytes": "3.1.2", 966 | "http-errors": "2.0.0", 967 | "iconv-lite": "0.4.24", 968 | "unpipe": "1.0.0" 969 | }, 970 | "engines": { 971 | "node": ">= 0.8" 972 | } 973 | }, 974 | "node_modules/readable-stream": { 975 | "version": "2.3.8", 976 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 977 | "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 978 | "dependencies": { 979 | "core-util-is": "~1.0.0", 980 | "inherits": "~2.0.3", 981 | "isarray": "~1.0.0", 982 | "process-nextick-args": "~2.0.0", 983 | "safe-buffer": "~5.1.1", 984 | "string_decoder": "~1.1.1", 985 | "util-deprecate": "~1.0.1" 986 | } 987 | }, 988 | "node_modules/readable-stream/node_modules/safe-buffer": { 989 | "version": "5.1.2", 990 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 991 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 992 | }, 993 | "node_modules/safe-buffer": { 994 | "version": "5.2.1", 995 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 996 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 997 | "funding": [ 998 | { 999 | "type": "github", 1000 | "url": "https://github.com/sponsors/feross" 1001 | }, 1002 | { 1003 | "type": "patreon", 1004 | "url": "https://www.patreon.com/feross" 1005 | }, 1006 | { 1007 | "type": "consulting", 1008 | "url": "https://feross.org/support" 1009 | } 1010 | ] 1011 | }, 1012 | "node_modules/safer-buffer": { 1013 | "version": "2.1.2", 1014 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1015 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1016 | }, 1017 | "node_modules/sax": { 1018 | "version": "1.4.1", 1019 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", 1020 | "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" 1021 | }, 1022 | "node_modules/semver": { 1023 | "version": "7.6.3", 1024 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", 1025 | "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", 1026 | "bin": { 1027 | "semver": "bin/semver.js" 1028 | }, 1029 | "engines": { 1030 | "node": ">=10" 1031 | } 1032 | }, 1033 | "node_modules/send": { 1034 | "version": "0.19.0", 1035 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 1036 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 1037 | "dependencies": { 1038 | "debug": "2.6.9", 1039 | "depd": "2.0.0", 1040 | "destroy": "1.2.0", 1041 | "encodeurl": "~1.0.2", 1042 | "escape-html": "~1.0.3", 1043 | "etag": "~1.8.1", 1044 | "fresh": "0.5.2", 1045 | "http-errors": "2.0.0", 1046 | "mime": "1.6.0", 1047 | "ms": "2.1.3", 1048 | "on-finished": "2.4.1", 1049 | "range-parser": "~1.2.1", 1050 | "statuses": "2.0.1" 1051 | }, 1052 | "engines": { 1053 | "node": ">= 0.8.0" 1054 | } 1055 | }, 1056 | "node_modules/send/node_modules/encodeurl": { 1057 | "version": "1.0.2", 1058 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1059 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 1060 | "engines": { 1061 | "node": ">= 0.8" 1062 | } 1063 | }, 1064 | "node_modules/send/node_modules/ms": { 1065 | "version": "2.1.3", 1066 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1067 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1068 | }, 1069 | "node_modules/serve-static": { 1070 | "version": "1.16.2", 1071 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 1072 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 1073 | "dependencies": { 1074 | "encodeurl": "~2.0.0", 1075 | "escape-html": "~1.0.3", 1076 | "parseurl": "~1.3.3", 1077 | "send": "0.19.0" 1078 | }, 1079 | "engines": { 1080 | "node": ">= 0.8.0" 1081 | } 1082 | }, 1083 | "node_modules/set-function-length": { 1084 | "version": "1.2.2", 1085 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 1086 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 1087 | "dependencies": { 1088 | "define-data-property": "^1.1.4", 1089 | "es-errors": "^1.3.0", 1090 | "function-bind": "^1.1.2", 1091 | "get-intrinsic": "^1.2.4", 1092 | "gopd": "^1.0.1", 1093 | "has-property-descriptors": "^1.0.2" 1094 | }, 1095 | "engines": { 1096 | "node": ">= 0.4" 1097 | } 1098 | }, 1099 | "node_modules/setprototypeof": { 1100 | "version": "1.2.0", 1101 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1102 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1103 | }, 1104 | "node_modules/side-channel": { 1105 | "version": "1.0.6", 1106 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 1107 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 1108 | "dependencies": { 1109 | "call-bind": "^1.0.7", 1110 | "es-errors": "^1.3.0", 1111 | "get-intrinsic": "^1.2.4", 1112 | "object-inspect": "^1.13.1" 1113 | }, 1114 | "engines": { 1115 | "node": ">= 0.4" 1116 | }, 1117 | "funding": { 1118 | "url": "https://github.com/sponsors/ljharb" 1119 | } 1120 | }, 1121 | "node_modules/statuses": { 1122 | "version": "2.0.1", 1123 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1124 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1125 | "engines": { 1126 | "node": ">= 0.8" 1127 | } 1128 | }, 1129 | "node_modules/streamsearch": { 1130 | "version": "1.1.0", 1131 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 1132 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 1133 | "engines": { 1134 | "node": ">=10.0.0" 1135 | } 1136 | }, 1137 | "node_modules/string_decoder": { 1138 | "version": "1.1.1", 1139 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1140 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1141 | "dependencies": { 1142 | "safe-buffer": "~5.1.0" 1143 | } 1144 | }, 1145 | "node_modules/string_decoder/node_modules/safe-buffer": { 1146 | "version": "5.1.2", 1147 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1148 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1149 | }, 1150 | "node_modules/toidentifier": { 1151 | "version": "1.0.1", 1152 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1153 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1154 | "engines": { 1155 | "node": ">=0.6" 1156 | } 1157 | }, 1158 | "node_modules/type-is": { 1159 | "version": "1.6.18", 1160 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1161 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1162 | "dependencies": { 1163 | "media-typer": "0.3.0", 1164 | "mime-types": "~2.1.24" 1165 | }, 1166 | "engines": { 1167 | "node": ">= 0.6" 1168 | } 1169 | }, 1170 | "node_modules/typedarray": { 1171 | "version": "0.0.6", 1172 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1173 | "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" 1174 | }, 1175 | "node_modules/unpipe": { 1176 | "version": "1.0.0", 1177 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1178 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1179 | "engines": { 1180 | "node": ">= 0.8" 1181 | } 1182 | }, 1183 | "node_modules/util-deprecate": { 1184 | "version": "1.0.2", 1185 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1186 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1187 | }, 1188 | "node_modules/utils-merge": { 1189 | "version": "1.0.1", 1190 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1191 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1192 | "engines": { 1193 | "node": ">= 0.4.0" 1194 | } 1195 | }, 1196 | "node_modules/uuid": { 1197 | "version": "8.3.2", 1198 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 1199 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 1200 | "bin": { 1201 | "uuid": "dist/bin/uuid" 1202 | } 1203 | }, 1204 | "node_modules/vary": { 1205 | "version": "1.1.2", 1206 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1207 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1208 | "engines": { 1209 | "node": ">= 0.8" 1210 | } 1211 | }, 1212 | "node_modules/xml2js": { 1213 | "version": "0.6.2", 1214 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", 1215 | "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", 1216 | "dependencies": { 1217 | "sax": ">=0.6.0", 1218 | "xmlbuilder": "~11.0.0" 1219 | }, 1220 | "engines": { 1221 | "node": ">=4.0.0" 1222 | } 1223 | }, 1224 | "node_modules/xmlbuilder": { 1225 | "version": "11.0.1", 1226 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 1227 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", 1228 | "engines": { 1229 | "node": ">=4.0" 1230 | } 1231 | }, 1232 | "node_modules/xtend": { 1233 | "version": "4.0.2", 1234 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1235 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 1236 | "engines": { 1237 | "node": ">=0.4" 1238 | } 1239 | } 1240 | } 1241 | } 1242 | -------------------------------------------------------------------------------- /test-generation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-generation", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@azure/msal-node": "^2.15.0", 14 | "axios": "^1.7.7", 15 | "child_process": "^1.0.2", 16 | "dotenv": "^16.4.5", 17 | "edge": "^7.10.1", 18 | "express": "^4.21.1", 19 | "fs": "^0.0.1-security", 20 | "multer": "^1.4.5-lts.1", 21 | "xml2js": "^0.6.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-generation/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Power BI Region, Workspaces, Reports, and Bookmarks 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 |

Generate Test Cases

24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 |
43 | 44 |
45 | 46 | 742 | 743 | 744 | -------------------------------------------------------------------------------- /tests/paginated.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { chromium } from 'playwright'; 3 | import { getAccessToken, getPaginatedEmbedToken, TestSettings, getAPIEndpoints, PaginatedEmbedInfo } from '../helper-functions/token-helpers'; 4 | // Used for local testings 5 | import { readJSONFilesFromFolder as readJSONFilesFromFolder } from '../helper-functions/file-reader'; 6 | import { logToConsole } from '../helper-functions/logging'; 7 | 8 | /* VARIABLES */ 9 | if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET || !process.env.TENANT_ID || !process.env.ENVIRONMENT) { 10 | throw new Error('Missing required environment variables.'); 11 | } 12 | 13 | // Initialize the environment variables 14 | let testRecords: Array; 15 | let endPoints; 16 | // Access environment variables 17 | let testSettings: TestSettings = { 18 | clientId: process.env.CLIENT_ID, 19 | clientSecret: process.env.CLIENT_SECRET, 20 | tenantId: process.env.TENANT_ID, 21 | environment: process.env.ENVIRONMENT as Environment 22 | }; 23 | 24 | // Parse the JSON file locally 25 | testRecords = readJSONFilesFromFolder('./test-cases'); 26 | //testRecords = testRecords.slice(0,2); 27 | endPoints = getAPIEndpoints(testSettings.environment); 28 | // Set logging for debugging 29 | let isVerboseLogging: boolean = true; 30 | // Get Access Token 31 | let testAccessToken: string | undefined = "" 32 | 33 | // Make sure to get token 34 | // Assume testing takes less then an hour at this point (before token expires) 35 | test.beforeAll(async () => { 36 | testAccessToken = await getAccessToken(testSettings); 37 | //isVerboseLogging = true; 38 | }); 39 | 40 | /* TESTS */ 41 | 42 | // Test to check if the access token is accessible 43 | test('test if access token can be generated from the environment variables provided (Paginated Report).', async ({ }) => { 44 | logToConsole('##[debug]Test if access token can be generated from the environment variables provided (Paginated Report).', isVerboseLogging); 45 | const token = testAccessToken 46 | //logToConsole('******** ' + token + '********', isVerboseLogging); 47 | expect(token).not.toBeUndefined(); 48 | }); 49 | 50 | // Test each record to check if the embed token is accessible 51 | testRecords.forEach((record) => { 52 | test(`test ${record.test_case} - '${record.report_name}' embed token is accessible ${record.role != '' && record.role !== undefined ? "(Role: " + record.role + ") " : ''}`, async ({ }) => { 53 | logToConsole(`##[debug]test ${record.test_case} - '${record.report_name}' embed token is accessible ${record.role != '' && record.role !== undefined ? "(Role: " + record.role + ") " : ''}`, isVerboseLogging); 54 | const tmpEmbedInfo: PaginatedEmbedInfo = { 55 | datasets: record.dataset_ids, 56 | reports: [{ id: record.report_id }] 57 | }; 58 | 59 | //logToConsole('------' + testAccessToken + '-------', isVerboseLogging); 60 | const embedToken = await getPaginatedEmbedToken(tmpEmbedInfo, endPoints, testAccessToken); 61 | expect(embedToken).not.toBeUndefined(); 62 | }); 63 | }// end for 64 | );// end test 65 | 66 | // Test for visual errors 67 | testRecords.forEach((record) => { 68 | test(`test ${record.test_case} - '${record.report_name}' for visual errors ${record.role != '' && record.role !== undefined ? "(Role: " + record.role + ") " : ''}, Link: ${endPoints.webPrefix}/groups/${record.workspace_id}/rdlreports/${record.report_id}${record.report_parameters_string != '' && record.report_parameters_string !== undefined ? "?" + record.report_parameters_string : ''} `, async ({ browser }) => { 69 | logToConsole(`##[debug]test ${record.test_case} - '${record.report_name}' for visual errors ${record.role != '' && record.role !== undefined ? "(Role: " + record.role + ") " : ''}, Link: ${endPoints.webPrefix}/groups/${record.workspace_id}/rdlreports/${record.report_id}${record.report_parameters_string != '' && record.report_parameters_string !== undefined ? "?" + record.report_parameters_string : ''}`,isVerboseLogging); 70 | //logToConsole(record, isVerboseLogging); 71 | //const accessToken = await getAccessToken(testSettings); 72 | browser = await chromium.launch({ args: ['--disable-web-security'], headless: false }); 73 | const context = await browser.newContext(); 74 | const page = await context.newPage(); 75 | 76 | // Get the embedURL for the paginated report 77 | const reportResponse = await fetch(`${endPoints.apiPrefix}/v1.0/myorg/groups/${record.workspace_id}/reports/${record.report_id}`, { 78 | method: 'GET', 79 | headers: { 80 | 'Authorization': `Bearer ${testAccessToken}` 81 | } 82 | }).then(res => res.json()); 83 | 84 | await page.goto('about:blank'); 85 | await page.addScriptTag({ url: 'https://cdnjs.cloudflare.com/ajax/libs/powerbi-client/2.23.7/powerbi.min.js' }); 86 | 87 | const tmpEmbedInfo: PaginatedEmbedInfo = { 88 | datasets: record.dataset_ids, 89 | reports: [{ id: record.report_id }] 90 | }; 91 | 92 | const embedToken = await getPaginatedEmbedToken(tmpEmbedInfo, endPoints, testAccessToken); 93 | 94 | let reportInfo = { 95 | reportId: record.report_id, 96 | parameterValues: record.report_parameters, 97 | embedUrl: reportResponse.embedUrl, // use one from report response 98 | embedToken: embedToken, 99 | endpoints: endPoints, 100 | waitSeconds: record.wait_seconds || 20 101 | }; 102 | 103 | // Evaluate the page and check for visual errors 104 | let test = await page.evaluate(async (reportInfo: any) => { 105 | var pbi = window['powerbi-client']; 106 | var models = window['powerbi-client'].models; 107 | let embedConfiguration: any = { 108 | type: 'report', 109 | id: reportInfo.reportId, 110 | embedUrl: reportInfo.embedUrl, 111 | accessToken: reportInfo.embedToken, 112 | tokenType: models.TokenType.Embed, 113 | permissions: models.Permissions.Read, 114 | viewMode: models.ViewMode.View, 115 | parameterValues: reportInfo.parameterValues 116 | }; 117 | 118 | // Add parameters if they exist 119 | if(reportInfo.parameterValues){ 120 | embedConfiguration['parameterValues'] = reportInfo.parameterValues; 121 | } 122 | 123 | // Initialize the powerbi service 124 | const powerbi = new pbi.service.Service(pbi.factories.hpmFactory, pbi.factories.wpmpFactory, pbi.factories.routerFactory); 125 | let embed = powerbi.embed(document.body, embedConfiguration); 126 | 127 | // Wait for the report to typically load 128 | let pauseProme = new Promise((resolve) => { 129 | setTimeout(() => { 130 | resolve(); 131 | }, reportInfo.waitSeconds * 1000); 132 | }); 133 | let result = await pauseProme; 134 | return result; 135 | }, reportInfo) 136 | 137 | await page.waitForLoadState('networkidle'); // Ensure all JS-loaded content is finished 138 | // Get the main page's HTML 139 | let fullPageHTML = await page.content(); 140 | // Find all iframes on the page 141 | const frames = page.frames().filter(frame => frame !== page.mainFrame()); 142 | 143 | for (const frame of frames) { 144 | const frameContent = await frame.evaluate(() => document.documentElement.outerHTML); 145 | const frameUrl = frame.url(); 146 | fullPageHTML += `\n\n${frameContent}\n`; 147 | } 148 | 149 | const searchForModal = fullPageHTML.search('"ms-Dialog-content'); 150 | expect(searchForModal).toBe(-1); 151 | }); 152 | }); -------------------------------------------------------------------------------- /tests/pbi.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { chromium } from 'playwright'; 3 | import { 4 | getAccessToken, 5 | TestSettings, getAPIEndpoints, 6 | getReportEmbedToken, 7 | createReportEmbedInfo 8 | 9 | } from '../helper-functions/token-helpers'; 10 | // Used for local testings 11 | import { IReportEmbedConfiguration } from 'powerbi-client'; 12 | import { readCSVFilesFromFolder } from '../helper-functions/file-reader'; 13 | import { logToConsole } from '../helper-functions/logging'; 14 | 15 | /* VARIABLES */ 16 | if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET || !process.env.TENANT_ID || !process.env.ENVIRONMENT) { 17 | throw new Error('Missing required environment variables.'); 18 | } 19 | 20 | // Initialize the environment variables 21 | let testRecords: Array; 22 | let endPoints; 23 | // Access environment variables 24 | let testSettings: TestSettings = { 25 | clientId: process.env.CLIENT_ID, 26 | clientSecret: process.env.CLIENT_SECRET, 27 | tenantId: process.env.TENANT_ID, 28 | environment: process.env.ENVIRONMENT as Environment 29 | }; 30 | 31 | // Parse the JSON file locally 32 | testRecords = readCSVFilesFromFolder('./test-cases'); 33 | //testRecords = testRecords.slice(0,2); 34 | endPoints = getAPIEndpoints(testSettings.environment); 35 | // Set logging for debugging 36 | let isVerboseLogging: boolean = true; 37 | // Get Access Token 38 | let testAccessToken: string | undefined = "" 39 | 40 | // Make sure to get token 41 | // Assume testing takes less then an hour at this point (before token expires) 42 | test.beforeAll(async () => { 43 | testAccessToken = await getAccessToken(testSettings); 44 | //isVerboseLogging = true; 45 | }); 46 | 47 | /* TESTS */ 48 | 49 | // Test to check if the access token is accessible 50 | test('test if access token can be generated from the environment variables provided.', async ({ }) => { 51 | logToConsole('##[debug]Test if access token can be generated from the environment variables provided.', isVerboseLogging); 52 | const token = testAccessToken 53 | //logToConsole('******** ' + token + '********', isVerboseLogging); 54 | expect(token).not.toBeUndefined(); 55 | }); 56 | 57 | // Test each record to check if the embed token is accessible 58 | testRecords.forEach((record) => { 59 | test(`test ${record.test_case} - '${record.report_name}' embed token is accessible ${record.role != '' && record.role !== undefined ? "(Role: " + record.role + ") " : ''} ${record.bookmark_id != '' && record.bookmark_id !== undefined ? "(Bookmark: " + record.bookmark_name + ")" : ''}`, async ({ }) => { 60 | logToConsole(`##[debug]test ${record.test_case} - '${record.report_name}' embed token is accessible ${record.role != '' && record.role !== undefined ? "(Role: " + record.role + ") " : ''} ${record.bookmark_id != '' && record.bookmark_id !== undefined ? "(Bookmark: " + record.bookmark_name + ")" : ''}`, isVerboseLogging); 61 | 62 | const tmpEmbedInfo = createReportEmbedInfo(record); 63 | const embedToken = await getReportEmbedToken(tmpEmbedInfo, endPoints, testAccessToken); 64 | //logToConsole('------' + testAccessToken + '-------', isVerboseLogging); 65 | //const embedToken = await getEmbedToken(tmpEmbedInfo, endPoints, testAccessToken); 66 | expect(embedToken).not.toBeUndefined(); 67 | }); 68 | }// end for 69 | );// end test 70 | 71 | // Test for visual errors 72 | testRecords.forEach((record) => { 73 | test(`test ${record.test_case} - '${record.report_name}' for visual errors ${record.role != '' && record.role !== undefined ? "(Role: " + record.role + ") " : ''} ${record.bookmark_id != '' && record.bookmark_id !== undefined ? "(Bookmark: " + record.bookmark_name + ")" : ''}, Link: ${endPoints.webPrefix}/groups/${record.workspace_id}/reports/${record.report_id}/${record.page_id}${record.bookmark_id != '' && record.bookmark_id !== undefined ? "?bookmarkGuid=" + record.bookmark_id : ""} `, async ({ browser }) => { 74 | logToConsole(`##[debug]test ${record.test_case} - '${record.report_name}' for visual errors ${record.role != '' && record.role !== undefined ? "(Role: " + record.role + ") " : ''} ${record.bookmark_id != '' && record.bookmark_id !== undefined ? "(Bookmark: " + record.bookmark_name + ")" : ''}, Link: ${endPoints.webPrefix}/groups/${record.workspace_id}/reports/${record.report_id}/${record.page_id}${record.bookmark_id != '' && record.bookmark_id !== undefined ? "?bookmarkGuid=" + record.bookmark_id : ""}`, isVerboseLogging); 75 | const accessToken = await getAccessToken(testSettings); 76 | browser = await chromium.launch({ args: ['--disable-web-security'], headless: false }); 77 | const context = await browser.newContext(); 78 | const page = await context.newPage(); 79 | 80 | // Get the embedURL for the report 81 | const reportResponse = await fetch(`${endPoints.apiPrefix}/v1.0/myorg/groups/${record.workspace_id}/reports/${record.report_id}`, { 82 | method: 'GET', 83 | headers: { 84 | 'Authorization': `Bearer ${testAccessToken}` 85 | } 86 | }).then(res => res.json()); 87 | 88 | await page.goto('about:blank'); 89 | await page.addScriptTag({ url: 'https://cdnjs.cloudflare.com/ajax/libs/powerbi-client/2.23.1/powerbi.min.js' }); 90 | 91 | const tmpEmbedInfo = createReportEmbedInfo(record); 92 | // Get the embedURL for the Power BI report 93 | const embedToken = await getReportEmbedToken(tmpEmbedInfo, endPoints, testAccessToken); 94 | 95 | let reportInfo = { 96 | reportId: record.report_id, 97 | page_id: record.page_id, 98 | embedUrl: reportResponse.embedUrl, // use the one from the report response 99 | embedToken: embedToken, 100 | endpoints: endPoints 101 | }; 102 | 103 | // Handle the bookmark 104 | if (record.bookmark_id != '' && record.bookmark_id !== undefined) { 105 | reportInfo['bookmark_id'] = record.bookmark_id; 106 | } 107 | 108 | // Evaluate the page and check for visual errors 109 | let test = await page.evaluate(async (reportInfo: any) => { 110 | var pbi = window['powerbi-client']; 111 | var models = window['powerbi-client'].models; 112 | let embedConfiguration: IReportEmbedConfiguration = { 113 | type: 'report', 114 | id: reportInfo.reportId, 115 | pageName: reportInfo.page_id, 116 | embedUrl: reportInfo.embedUrl, 117 | accessToken: reportInfo.embedToken, 118 | tokenType: models.TokenType.Embed, 119 | permissions: models.Permissions.Read, 120 | viewMode: models.ViewMode.View 121 | }; 122 | 123 | // Apply bookmark if it exists 124 | if (reportInfo.bookmark_id && reportInfo.bookmark_id.trim() !== "") { 125 | embedConfiguration = { 126 | type: 'report', 127 | id: reportInfo.reportId, 128 | pageName: reportInfo.page_id, 129 | bookmark: { 130 | name: reportInfo.bookmark_id 131 | }, 132 | embedUrl: reportInfo.embedUrl, 133 | accessToken: reportInfo.embedToken, 134 | tokenType: models.TokenType.Embed, 135 | permissions: models.Permissions.Read, 136 | viewMode: models.ViewMode.View 137 | }; 138 | }// apply bookmark 139 | 140 | // Initialize the powerbi service 141 | const powerbi = new pbi.service.Service(pbi.factories.hpmFactory, pbi.factories.wpmpFactory, pbi.factories.routerFactory); 142 | let embed = powerbi.embed(document.body, embedConfiguration); 143 | 144 | // Wait for the report to render or error out using the promises 145 | const once = { 146 | once: true, 147 | }; 148 | let testErrorPromise = new Promise((resolve) => { 149 | document.body.addEventListener('error', async function (event: any) { 150 | resolve(event); 151 | }, once); 152 | }); 153 | let testRenderedPromise = new Promise((resolve) => { 154 | document.body.addEventListener('rendered', async function (event: any) { 155 | resolve();// resolve undefined 156 | }, once); 157 | }); 158 | 159 | // Wait for the report to render or error out using the race condition 160 | let result = await Promise.race([testErrorPromise, testRenderedPromise]); 161 | return result === undefined ? "passed" : "failed"; 162 | 163 | }, reportInfo) 164 | 165 | expect(test).toBe("passed"); 166 | }); 167 | }); --------------------------------------------------------------------------------