diff --git a/.github/actions/e2e-report/action.yml b/.github/actions/e2e-report/action.yml
new file mode 100644
index 00000000000..08f2e6974bc
--- /dev/null
+++ b/.github/actions/e2e-report/action.yml
@@ -0,0 +1,117 @@
+name: "E2E Report"
+description: "Generate and upload E2E reports, and notify stakeholders"
+inputs:
+ aws-s3-bucket:
+ description: "AWS S3 bucket name"
+ required: true
+ aws-role:
+ description: "AWS role to assume"
+ required: true
+ junit-pattern:
+ description: "Pattern to match JUnit reports"
+ default: "junit-report-*"
+ blob-pattern:
+ description: "Pattern to match blob reports"
+ default: "blob-report-*"
+ slack-token:
+ description: "Slack token for notifications"
+ required: true
+ slack-channel:
+ description: "Slack channel for notifications"
+ required: true
+ slack-title:
+ description: "Slack title for the test suite"
+ default: "Test Suite Results"
+ testrail-api-key:
+ description: "TestRail API Key"
+ required: true
+ github-token:
+ description: "GitHub token for API calls"
+ required: true
+ testrail-project:
+ description: "TestRail project name"
+ default: "Positron"
+ testrail-title:
+ description: "Title for TestRail runs"
+ default: "E2E Test Run"
+
+runs:
+ using: "composite"
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+
+ - name: Install AWS CLI
+ shell: bash
+ run: |
+ apt-get update && apt-get install -y unzip python3-pip
+ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
+ unzip -q /tmp/awscliv2.zip -d /tmp
+ /tmp/aws/install -i $HOME/aws-cli -b $HOME/bin
+ rm -rf /tmp/aws /tmp/awscliv2.zip
+
+ - name: Add AWS CLI to PATH
+ shell: bash
+ run: echo "$HOME/bin" >> $GITHUB_PATH
+
+ - name: Verify AWS CLI installation
+ shell: bash
+ run: aws --version
+
+ - name: Download Blob Reports
+ uses: actions/download-artifact@v4
+ with:
+ path: all-blob-reports
+ pattern: ${{ inputs.blob-pattern }}
+ merge-multiple: true
+
+ - name: Merge Blobs into HTML Report
+ shell: bash
+ run: npx playwright merge-reports --reporter html ./all-blob-reports
+
+ - name: Upload Playwright Report to S3
+ uses: ./.github/actions/upload-report-to-s3
+ with:
+ role-to-assume: ${{ inputs.aws-role }}
+
+ - name: Send HTML Report URL to GitHub Summary
+ shell: bash
+ run: |
+ REPORT_URL="https://d38p2avprg8il3.cloudfront.net/${{ env.REPORT_DIR }}/index.html"
+ echo "Report URL: $REPORT_URL"
+ echo "📄 [Playwright Report]($REPORT_URL)
" > $GITHUB_STEP_SUMMARY
+
+ - name: Download JUnit Reports
+ uses: actions/download-artifact@v4
+ with:
+ path: all-junit-reports
+ pattern: ${{ inputs.junit-pattern }}
+ merge-multiple: true
+
+ - name: Merge JUnit Reports
+ shell: bash
+ run: npm install -g junit-report-merger && npx jrm junit.xml "all-junit-reports/*.xml"
+
+ - name: Install trcli
+ shell: bash
+ run: apt-get update && apt-get install -y python3-pip && pip3 install trcli
+
+ - name: Upload Test Results to TestRail
+ shell: bash
+ run: |
+ TESTRAIL_TITLE="$(date +'%Y-%m-%d') ${{ inputs.testrail-title }} - $GITHUB_REF_NAME"
+ echo "TESTRAIL_TITLE=$TESTRAIL_TITLE" >> $GITHUB_ENV
+ trcli --host "https://posit.testrail.io/" --project "${{ inputs.testrail-project }}" --username testrailautomation@posit.co --key "${{ inputs.testrail-api-key }}" parse_junit --file "./junit.xml" --case-matcher name --title "$TESTRAIL_TITLE" --close-run
+
+ - name: Send Slack Notification
+ uses: testlabauto/action-test-results-to-slack@v0.0.6
+ with:
+ github_token: ${{ inputs.github-token }}
+ slack_token: ${{ inputs.slack-token }}
+ slack_channel: ${{ inputs.slack-channel }}
+ suite_name: ${{ inputs.slack-title }}
diff --git a/.github/actions/setup-test-env/action.yml b/.github/actions/setup-test-env/action.yml
index 3f88e1de10e..707f0d89e3e 100644
--- a/.github/actions/setup-test-env/action.yml
+++ b/.github/actions/setup-test-env/action.yml
@@ -14,6 +14,10 @@ inputs:
runs:
using: "composite"
steps:
+ - name: Compile E2E Tests
+ shell: bash
+ run: yarn --cwd test/automation compile && yarn --cwd test/smoke compile
+
- name: Setup AWS S3 Access
uses: aws-actions/configure-aws-credentials@v4
with:
diff --git a/.github/actions/upload-report-to-s3/action.yml b/.github/actions/upload-report-to-s3/action.yml
new file mode 100644
index 00000000000..0dec03de8bc
--- /dev/null
+++ b/.github/actions/upload-report-to-s3/action.yml
@@ -0,0 +1,22 @@
+name: "Upload Playwright Report to S3"
+description: "Configures AWS credentials and uploads the Playwright report to S3"
+inputs:
+ role-to-assume:
+ description: "The AWS role to assume"
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ if: ${{ !cancelled() }}
+ with:
+ role-to-assume: ${{ inputs.role-to-assume }}
+ aws-region: "us-east-1"
+
+ - name: Upload Playwright Report to S3 Bucket
+ if: ${{ !cancelled() }}
+ shell: bash
+ run: |
+ aws s3 cp playwright-report/. s3://positron-test-reports/playwright-report-${{ github.run_id }} --recursive
diff --git a/.github/workflows/smoke-test-release-run-ubuntu.yml b/.github/workflows/e2e-test-release-run-ubuntu.yml
similarity index 73%
rename from .github/workflows/smoke-test-release-run-ubuntu.yml
rename to .github/workflows/e2e-test-release-run-ubuntu.yml
index 4dcd0de48e8..0daf3a0d6e5 100644
--- a/.github/workflows/smoke-test-release-run-ubuntu.yml
+++ b/.github/workflows/e2e-test-release-run-ubuntu.yml
@@ -2,11 +2,21 @@ name: "Latest Release: E2E Electron Tests"
on:
workflow_dispatch:
+ inputs:
+ e2e_grep:
+ required: false
+ description: "Grep filter to apply to the e2e tests: @pr, @win, etc."
+ default: ""
+ type: string
permissions:
id-token: write
contents: read
+env:
+ E2E_GREP: ${{ inputs.e2e_grep || '' }}
+ REPORT_DIR: playwright-report-${{ github.run_id }}
+
jobs:
linux:
name: e2e-electron-tests
@@ -66,36 +76,26 @@ jobs:
sudo update-rc.d xvfb defaults
sudo service xvfb start
+ - name: Send HTML report URL to GitHub Summary
+ if: ${{ !cancelled() }}
+ run: |
+ REPORT_URL="https://d38p2avprg8il3.cloudfront.net/${{ env.REPORT_DIR }}/index.html"
+ echo "Report URL: $REPORT_URL"
+ echo "📄 [Playwright Report]($REPORT_URL)
" > $GITHUB_STEP_SUMMARY
+
- name: Run Tests (Electron)
+ if: ${{ !cancelled() }}
env:
POSITRON_PY_VER_SEL: 3.10.12
POSITRON_R_VER_SEL: 4.4.0
id: electron-smoke-tests
run: |
- cd test/smoke
- export BUILD_ARTIFACTSTAGINGDIRECTORY=../../.build/logs/smoke-tests-electron
export DISPLAY=:10
- node run-tests.js --tracing --parallel --jobs 2 --skip-cleanup --build /usr/share/positron
+ BUILD=/usr/share/positron npx playwright test --project e2e-electron --workers 2 --grep="${{ env.E2E_GREP }}"
- - name: Convert XUnit to JUnit
- id: xunit-to-junit
- if: success() || failure()
- run: |
- sudo apt-get update
- sudo apt-get install -y libxml2-utils
- yarn xunit-to-junit smoke-tests-electron
-
- - name: Publish Electron Test Report
- uses: mikepenz/action-junit-report@v4
- if: success() || failure()
+ - name: Upload Playwright Report to S3
+ if: ${{ !cancelled() }}
+ uses: ./.github/actions/upload-report-to-s3
with:
- report_paths: "**/.build/logs/smoke-tests-electron/test-results/xunit-results.xml"
- check_name: "Electron Test Results"
- token: ${{ secrets.GITHUB_TOKEN }}
+ role-to-assume: ${{ secrets.AWS_TEST_REPORTS_ROLE }}
- - name: Upload Artifacts - Electron
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: run-artifacts-electron
- path: .build/logs/smoke-tests-electron/
diff --git a/.github/workflows/positron-full-test.yml b/.github/workflows/positron-full-test.yml
index 476d38c1eec..8a7adf73ead 100644
--- a/.github/workflows/positron-full-test.yml
+++ b/.github/workflows/positron-full-test.yml
@@ -95,7 +95,12 @@ jobs:
e2e-electron-tests:
runs-on: ubuntu-latest-8x
- timeout-minutes: 80
+ timeout-minutes: 60
+ strategy:
+ fail-fast: false
+ matrix:
+ shardIndex: [1, 2]
+ shardTotal: [2]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
POSITRON_BUILD_NUMBER: 0 # CI skips building releases
@@ -126,45 +131,27 @@ jobs:
env:
POSITRON_PY_VER_SEL: 3.10.12
POSITRON_R_VER_SEL: 4.4.0
- id: electron-smoke-tests
- run: DISPLAY=:10 yarn smoketest-all --tracing --skip-cleanup
-
- - name: Convert XUnit to JUnit
- id: xunit-to-junit
- if: success() || failure()
- run: |
- sudo apt-get update
- sudo apt-get install -y libxml2-utils
- yarn xunit-to-junit smoke-tests-electron
-
- - name: Publish Electron Test Report
- uses: mikepenz/action-junit-report@v4
- if: success() || failure()
- with:
- report_paths: "**/.build/logs/smoke-tests-electron/test-results/xunit-results.xml"
- check_name: "Electron Test Results"
- token: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Set TestRail Run Title
- id: set-testrail-run-title
- if: steps.xunit-to-junit == 'success'
- run: echo "TESTRAIL_TITLE=$(date +'%Y-%m-%d') Nightly Smoke Tests - $GITHUB_REF_NAME" >> $GITHUB_ENV
+ id: electron-tests
+ run: DISPLAY=:10 npx playwright test --project e2e-electron --workers 1 --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- - name: Upload Test Results to TestRail (Electron ONLY)
- id: testrail-upload
- if: steps.xunit-to-junit == 'success'
- run: trcli --host "https://posit.testrail.io/" --project Positron --username testrailautomation@posit.co --key ${{ secrets.TESTRAIL_API_KEY}} parse_junit --file ".build/logs/smoke-tests-electron/test-results/xunit-results.xml" --case-matcher name --title "$TESTRAIL_TITLE" --close-run
+ - name: Upload blob report
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: blob-report-electron-${{ matrix.shardIndex }}
+ path: blob-report
+ retention-days: 14
- - name: Upload Artifacts - Electron
- if: always()
+ - name: Upload junit report
+ if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
- name: run-artifacts-electron
- path: .build/logs/smoke-tests-electron/
+ name: junit-report-electron-${{ matrix.shardIndex }}
+ path: test-results/junit.xml
e2e-browser-tests:
runs-on: ubuntu-latest-4x
- timeout-minutes: 40
+ timeout-minutes: 50
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
POSITRON_BUILD_NUMBER: 0 # CI skips building releases
@@ -197,56 +184,50 @@ jobs:
aws-region: ${{ secrets.QA_AWS_REGION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- - name: Run Web Smoke Tests
+ - name: Run Tests (Browser)
env:
POSITRON_PY_VER_SEL: 3.10.12
POSITRON_R_VER_SEL: 4.4.0
- id: electron-web-smoke-tests
- run: DISPLAY=:10 yarn smoketest-web --tracing
-
- - name: Convert XUnit to JUnit
- id: xunit-to-junit
- if: success() || failure()
- run: |
- sudo apt-get update
- sudo apt-get install -y libxml2-utils
- yarn xunit-to-junit smoke-tests-browser
-
- - name: Publish Web Test Report
- uses: mikepenz/action-junit-report@v4
- if: success() || failure()
- with:
- report_paths: "**/.build/logs/smoke-tests-browser/test-results/xunit-results.xml"
- check_name: "Web Test Results"
- token: ${{ secrets.GITHUB_TOKEN }}
+ id: browser-tests
+ run: DISPLAY=:10 npx playwright test --project e2e-browser --workers 1
- - name: Upload Artifacts - Browser
- if: always()
+ - name: Upload blob report
+ if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
- name: run-artifacts-browser
- path: .build/logs/smoke-tests-browser/
+ name: blob-report-browser
+ path: blob-report
+ retention-days: 14
- name: Clean up license files
if: always()
run: cd .. && rm -rf positron-license
- slack-notification:
- name: "slack-notification"
+ e2e-report:
+ if: always()
runs-on: ubuntu-latest
- needs:
- [
- "unit-tests",
- "integration-tests",
- "e2e-electron-tests",
- "e2e-browser-tests",
- ]
- if: failure()
+ container:
+ image: mcr.microsoft.com/playwright:v1.48.0-jammy
+ needs: [e2e-electron-tests, e2e-browser-tests, unit-tests, integration-tests]
+ env:
+ REPORT_DIR: playwright-report-${{ github.run_id }}
+
steps:
- - name: "Send Slack notification"
- uses: testlabauto/action-test-results-to-slack@v0.0.6
+ - uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
with:
- github_token: ${{ secrets.POSITRON_GITHUB_PAT }}
- slack_token: ${{ secrets.SMOKE_TESTS_SLACK_TOKEN }}
- slack_channel: C07FR1JNZNJ #positron-test-results channel
- suite_name: Positron Full Test Suite
+ node-version-file: .nvmrc
+
+ - name: Run E2E Report
+ uses: ./.github/actions/e2e-report
+ with:
+ aws-s3-bucket: positron-test-reports
+ aws-role: ${{ secrets.AWS_TEST_REPORTS_ROLE }}
+ slack-token: ${{ secrets.SMOKE_TESTS_SLACK_TOKEN }}
+ slack-channel: C07FR1JNZNJ
+ slack-title: "Full Test Suite (Electron)"
+ testrail-api-key: ${{ secrets.TESTRAIL_API_KEY }}
+ testrail-title: "E2E Electron Test Run"
+ github-token: ${{ secrets.POSITRON_GITHUB_PAT }}
diff --git a/.github/workflows/positron-merge-to-branch.yml b/.github/workflows/positron-merge-to-branch.yml
index ff2a56a5eb6..e02cee5e8f7 100644
--- a/.github/workflows/positron-merge-to-branch.yml
+++ b/.github/workflows/positron-merge-to-branch.yml
@@ -7,31 +7,81 @@ on:
- 'prerelease/**'
workflow_call:
inputs:
- smoketest_target:
+ e2e_grep:
required: false
- description: "Smoketest suite to run, e.g. smoketest-all or smoketest-pr"
- default: "smoketest-all"
+ description: "Grep filter to apply to the e2e tests: @pr, @win, etc."
+ default: ""
type: string
workflow_dispatch:
inputs:
- smoketest_target:
+ e2e_grep:
required: false
- description: "Smoketest suite to run, e.g. smoketest-all or smoketest-pr"
- default: "smoketest-all"
+ description: "Grep filter to apply to the e2e tests: @pr, @win, etc."
+ default: ""
type: string
env:
- SMOKETEST_TARGET: ${{ inputs.smoketest_target || 'smoketest-all' }}
+ E2E_GREP: ${{ inputs.e2e_grep || '' }}
permissions:
id-token: write
contents: read
jobs:
- linux:
- name: Tests on Linux
- runs-on: ubuntu-latest-8x
+ e2e-electron:
timeout-minutes: 45
+ runs-on: ubuntu-latest-8x
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ POSITRON_BUILD_NUMBER: 0 # CI skips building releases
+ _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false # this check can be flaky in the R pkg tests
+ _R_CHECK_CRAN_INCOMING_: false
+ _R_CHECK_SYSTEM_CLOCK_: false
+ AWS_S3_BUCKET: positron-test-reports
+ REPORT_DIR: playwright-report-${{ github.run_id }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+
+ - name: Cache node_modules, build, extensions, and remote
+ uses: ./.github/actions/cache-multi-paths
+
+ - name: Setup Build and Compile
+ uses: ./.github/actions/setup-build-env
+
+ - name: Setup E2E Test Environment
+ uses: ./.github/actions/setup-test-env
+ with:
+ aws-role-to-assume: ${{ secrets.QA_AWS_RO_ROLE }}
+ aws-region: ${{ secrets.QA_AWS_REGION }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Send HTML report URL to GitHub Summary
+ if: ${{ !cancelled() }}
+ run: |
+ REPORT_URL="https://d38p2avprg8il3.cloudfront.net/${{ env.REPORT_DIR }}/index.html"
+ echo "Report URL: $REPORT_URL"
+ echo "📄 [Playwright Report]($REPORT_URL)
" > $GITHUB_STEP_SUMMARY
+
+ - name: Run Playwright Tests (Electron)
+ env:
+ POSITRON_PY_VER_SEL: 3.10.12
+ POSITRON_R_VER_SEL: 4.4.0
+ id: e2e-playwright-tests
+ run: DISPLAY=:10 npx playwright test --project e2e-electron --workers 2 --grep="${{ env.E2E_GREP }}"
+
+ - name: Upload Playwright Report to S3
+ if: ${{ success() || failure() }}
+ uses: ./.github/actions/upload-report-to-s3
+ with:
+ role-to-assume: ${{ secrets.AWS_TEST_REPORTS_ROLE }}
+
+ unit-integration:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
POSITRON_BUILD_NUMBER: 0 # CI skips building releases
@@ -57,12 +107,11 @@ jobs:
github-token: ${{ secrets.POSITRON_GITHUB_PAT }}
license-key: ${{ secrets.POSITRON_DEV_LICENSE }}
- - name: Setup E2E Test Environment
- uses: ./.github/actions/setup-test-env
+ # one unit test needs this: Can list tables and fields from R connections
+ - name: Setup R
+ uses: ./.github/actions/install-r
with:
- aws-role-to-assume: ${{ secrets.QA_AWS_RO_ROLE }}
- aws-region: ${{ secrets.QA_AWS_REGION }}
- github-token: ${{ secrets.GITHUB_TOKEN }}
+ version: "4.4.0"
- name: Compile Integration Tests
run: yarn --cwd test/integration/browser compile
@@ -75,47 +124,11 @@ jobs:
id: electron-integration-tests
run: DISPLAY=:10 ./scripts/test-integration-pr.sh
- - name: Run Smoke Tests (Electron)
- if: success() || failure()
- env:
- POSITRON_PY_VER_SEL: 3.10.12
- POSITRON_R_VER_SEL: 4.4.0
- id: electron-smoke-tests
- run: DISPLAY=:10 yarn ${{ env.SMOKETEST_TARGET }} --tracing --parallel --jobs 2 --skip-cleanup
-
- - name: Convert XUnit to JUnit
- id: xunit-to-junit
- if: success() || failure()
- run: |
- sudo apt-get update
- sudo apt-get install -y libxml2-utils
- /usr/bin/xmllint --version
- yarn xunit-to-junit smoke-tests-electron
-
- - name: Set TestRail Run Title
- id: set-testrail-run-title
- if: always()
- run: echo "TESTRAIL_TITLE=$(date +'%Y-%m-%d') Smoke Tests on branch $GITHUB_REF_NAME" >> $GITHUB_ENV
-
- - name: Upload Test Results to TestRail
- id: testrail-upload
- if: always()
- run: trcli --host "https://posit.testrail.io/" --project Positron --username testrailautomation@posit.co --key ${{ secrets.TESTRAIL_API_KEY}} parse_junit --file ".build/logs/smoke-tests-electron/test-results/results.xml" --case-matcher name --title "$TESTRAIL_TITLE" --close-run
-
- - name: Upload run artifacts
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: run-artifacts
- path: .build/logs/smoke-tests-electron/
- outputs:
- target: ${{ env.SMOKETEST_TARGET }}
-
slack-notification:
name: "Send Slack notification"
runs-on: ubuntu-latest
- needs: linux
- if: ${{ failure() && needs.linux.outputs.target == 'smoketest-all' }}
+ needs: [unit-integration, e2e-electron]
+ if: ${{ failure() && inputs.e2e_grep == '' }}
steps:
- name: "Send Slack notification"
uses: testlabauto/action-test-results-to-slack@v0.0.6
diff --git a/.github/workflows/positron-pull-requests.yml b/.github/workflows/positron-pull-requests.yml
index ed29e38dfe3..9c49aa65ad0 100644
--- a/.github/workflows/positron-pull-requests.yml
+++ b/.github/workflows/positron-pull-requests.yml
@@ -7,8 +7,8 @@ on:
- 'prerelease/**'
jobs:
- positron-ci:
+ tests:
uses: ./.github/workflows/positron-merge-to-branch.yml
secrets: inherit
with:
- smoketest_target: smoketest-pr
+ e2e_grep: "@pr"
diff --git a/.github/workflows/positron-windows-nightly.yml b/.github/workflows/positron-windows-nightly.yml
index 6152fd3d1d1..354d0eda9bf 100644
--- a/.github/workflows/positron-windows-nightly.yml
+++ b/.github/workflows/positron-windows-nightly.yml
@@ -11,15 +11,14 @@ permissions:
contents: read
jobs:
- windows:
- name: Tests on Windows
+ e2e-windows-tests:
runs-on:
labels: [windows-latest-8x]
timeout-minutes: 60
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
POSITRON_BUILD_NUMBER: 0 # CI skips building releases
- BUILD_ARTIFACTSTAGINGDIRECTORY: ..\..\.build\logs\smoke-tests-electron
+ AWS_S3_BUCKET: positron-test-reports
steps:
- uses: actions/checkout@v4
@@ -75,7 +74,7 @@ jobs:
- name: Set up R
uses: r-lib/actions/setup-r@v2
with:
- r-version: '4.4.0'
+ r-version: "4.4.0"
- name: Install R packages
run: |
@@ -99,48 +98,54 @@ jobs:
role-to-assume: ${{ secrets.QA_AWS_RO_ROLE }}
aws-region: ${{ secrets.QA_AWS_REGION }}
- - name: Run Smoke Tests (Electron)
+ - name: Run Tests on Windows (Electron)
env:
POSITRON_PY_VER_SEL: 3.10.10
POSITRON_R_VER_SEL: 4.4.0
- id: electron-smoke-tests
- run: |
- Write-Output $env:BUILD_ARTIFACTSTAGINGDIRECTORY # debug
- cd test\smoke
- node run-tests.js --tracing --win
- Get-ChildItem -Recurse -Filter "*results.xml" # debug
-
- - name: Set TestRail Run Title
- id: set-testrail-run-title
- if: success() || failure()
- run: echo "TESTRAIL_TITLE=$(Get-Date -Format 'yyyy-MM-dd') Nightly Smoke Tests - $env:GITHUB_REF_NAME" >> $env:GITHUB_ENV
- shell: pwsh
-
- - name: Upload Test Results to TestRail
- id: testrail-upload
- if: success() || failure()
- shell: pwsh
- run: |
- trcli --host "https://posit.testrail.io/" --project Positron --username testrailautomation@posit.co --key ${{ secrets.TESTRAIL_API_KEY}} parse_junit --file ".build/logs/smoke-tests-electron/test-results/xunit-results.xml" --case-matcher name --title "$env:TESTRAIL_TITLE" --close-run
+ if: ${{ !cancelled() }}
+ id: e2e-win-electron-tests
+ run: npx playwright test --project e2e-electron --grep "@win" --workers 1
+ - name: Upload blob report
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: blob-report-electron-${{ matrix.shardIndex }}
+ path: blob-report
+ retention-days: 14
- - name: Upload run artifacts - electron
- if: always()
+ - name: Upload junit report
+ if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
- name: run-artifacts-electron
- path: .build/logs/smoke-tests-electron/
+ name: junit-report-electron-${{ matrix.shardIndex }}
+ path: test-results/junit.xml
- slack-notification:
- name: "Send Slack notification"
+ e2e-report:
+ if: always()
runs-on: ubuntu-latest
- needs: windows
- if: failure()
+ container:
+ image: mcr.microsoft.com/playwright:v1.48.0-jammy
+ needs: e2e-windows-tests
+ env:
+ REPORT_DIR: playwright-report-${{ github.run_id }}
+
steps:
- - name: "Send Slack notification"
- uses: testlabauto/action-test-results-to-slack@v0.0.6
+ - uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+
+ - name: Run E2E Report
+ uses: ./.github/actions/e2e-report
with:
- github_token: ${{ secrets.POSITRON_GITHUB_PAT }}
- slack_token: ${{ secrets.SMOKE_TESTS_SLACK_TOKEN }}
- slack_channel: C07FR1JNZNJ #positron-test-results channel
- suite_name: Positron Full Test Suite (Windows)
+ aws-s3-bucket: positron-test-reports
+ aws-role: ${{ secrets.AWS_TEST_REPORTS_ROLE }}
+ slack-token: ${{ secrets.SMOKE_TESTS_SLACK_TOKEN }}
+ slack-channel: C07FR1JNZNJ
+ slack-title: "Full Test Suite (Windows)"
+ testrail-api-key: ${{ secrets.TESTRAIL_API_KEY }}
+ testrail-title: "E2E Windows Test Run"
+ github-token: ${{ secrets.POSITRON_GITHUB_PAT }}
diff --git a/.gitignore b/.gitignore
index 232848ee7d8..c505ff7d92e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,5 +25,8 @@ product.overrides.json
# --- Start Positron ---
.Rproj.user
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+/test-logs/
# --- End Positron ---
-
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 23a5fd9ff08..5d9cc22fc3a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -511,63 +511,9 @@
"hidden": true
}
},
- {
- "type": "node",
- "request": "launch",
- "name": "Launch Smoke Test",
- // --- Start Positron
- "program": "${workspaceFolder}/test/smoke/run-tests.js",
- // --- End Positron
- "cwd": "${workspaceFolder}/test/smoke",
- "timeout": 240000,
- "args": [
- "--tracing"
- ],
- "outFiles": [
- "${cwd}/out/**/*.js"
- ],
- // --- Start Positron
- "sourceMaps": true,
- "resolveSourceMapLocations": [
- "${workspaceFolder}/**",
- "!**/node_modules/**"
- ],
- // --- End Positron
- "env": {
- "NODE_ENV": "development",
- "VSCODE_DEV": "1",
- "VSCODE_CLI": "1"
- }
- },
- {
- "type": "node",
- "request": "launch",
- "name": "Launch Web Smoke Test",
- // --- Start Positron
- "program": "${workspaceFolder}/test/smoke/run-tests.js",
- // --- End Positron
- "cwd": "${workspaceFolder}/test/smoke",
- "timeout": 240000,
- "args": [
- "--tracing",
- "--web"
- ],
- "outFiles": [
- "${cwd}/out/**/*.js"
- ],
- // --- Start Positron
- "sourceMaps": true,
- "resolveSourceMapLocations": [
- "${workspaceFolder}/**",
- "!**/node_modules/**"
- ],
- // --- End Positron
- "env": {
- "NODE_ENV": "development",
- "VSCODE_DEV": "1",
- "VSCODE_CLI": "1"
- }
- },
+ // --- Start Positron
+ // Removed `Launch Smoke Test` and `Launch Web Smoke Test` in favor of Playwright runner
+ // --- End Positron
{
"name": "Launch Built-in Extension",
"type": "extensionHost",
diff --git a/build/secrets/.secrets.baseline b/build/secrets/.secrets.baseline
index fd4c3c5db8c..75ffc29a385 100644
--- a/build/secrets/.secrets.baseline
+++ b/build/secrets/.secrets.baseline
@@ -139,7 +139,7 @@
"filename": ".github/workflows/positron-pull-requests.yml",
"hashed_secret": "3e26d6750975d678acb8fa35a0f69237881576b0",
"is_verified": false,
- "line_number": 11,
+ "line_number": 12,
"is_secret": false
}
],
@@ -1908,5 +1908,5 @@
}
]
},
- "generated_at": "2024-09-26T21:49:10Z"
+ "generated_at": "2024-11-18T21:27:12Z"
}
diff --git a/package.json b/package.json
index 05b4e8d314f..7261b20f6e3 100644
--- a/package.json
+++ b/package.json
@@ -44,14 +44,12 @@
"7z": "7z",
"update-grammars": "node build/npm/update-all-grammars.mjs",
"update-localization-extension": "node build/npm/update-localization-extension.js",
- "smoketest": "node build/lib/preLaunch.js && cd test/smoke && yarn compile && node run-tests.js",
- "smoketest-no-compile": "export BUILD_ARTIFACTSTAGINGDIRECTORY=../../.build/logs/smoke-tests-electron && cd test/smoke && node run-tests.js",
- "smoketest-all": "export BUILD_ARTIFACTSTAGINGDIRECTORY=../../.build/logs/smoke-tests-electron && yarn smoketest",
- "smoketest-pr": "export BUILD_ARTIFACTSTAGINGDIRECTORY=../../.build/logs/smoke-tests-electron && yarn smoketest --pr",
- "smoketest-web": "export BUILD_ARTIFACTSTAGINGDIRECTORY=../../.build/logs/smoke-tests-browser && yarn smoketest --web",
- "smoketest-win": "export BUILD_ARTIFACTSTAGINGDIRECTORY=../../.build/logs/smoke-tests-win && yarn smoketest --win",
- "smoketest-only": "export BUILD_ARTIFACTSTAGINGDIRECTORY=../../.build/logs/smoke-tests-electron && cd test/smoke && yarn compile && node run-tests.js --only",
- "xunit-to-junit": "sh ./scripts/convert-xunit-to-junit.sh",
+ "e2e": "yarn e2e-electron",
+ "e2e-electron": "npx playwright test --project e2e-electron",
+ "e2e-browser": "npx playwright test --project e2e-browser --workers 1",
+ "e2e-pr": "npx playwright test --project e2e-electron --grep @pr",
+ "e2e-win": "npx playwright test --project e2e-electron --grep @win",
+ "e2e-failed": "npx playwright test --last-failed",
"download-builtin-extensions": "node build/lib/builtInExtensions.js",
"download-builtin-extensions-cg": "node build/lib/builtInExtensionsCG.js",
"download-quarto": "node build/lib/quarto.js",
@@ -130,7 +128,8 @@
"yazl": "^2.4.3"
},
"devDependencies": {
- "@playwright/test": "^1.46.1",
+ "@midleman/github-actions-reporter": "^1.9.5",
+ "@playwright/test": "^1.49.0",
"@swc/core": "1.3.62",
"@types/cookie": "^0.3.3",
"@types/debug": "^4.1.5",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000000..125b4c2257a
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,69 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { defineConfig } from '@playwright/test';
+import { CustomTestOptions } from './test/smoke/src/areas/positron/_test.setup';
+import type { GitHubActionOptions } from '@midleman/github-actions-reporter';
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ globalSetup: require.resolve('./test/smoke/src/areas/positron/_global.setup.ts'),
+ testDir: './test/smoke/src/areas/positron',
+ testMatch: '*.test.ts',
+ fullyParallel: false, // Run individual tests w/in a spec in parallel
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 1 : 0,
+ workers: 3, // Number of parallel workers
+ timeout: 2 * 60 * 1000,
+ reportSlowTests: {
+ max: 10,
+ threshold: 60 * 1000, // 1 minute
+ },
+ reporter: process.env.CI
+ ? [
+ ['@midleman/github-actions-reporter', {
+ title: '',
+ useDetails: true,
+ showError: true,
+ showAnnotations: false,
+ includeResults: ['fail', 'flaky']
+ }],
+ ['junit', { outputFile: 'test-results/junit.xml' }],
+ ['list'], ['html'], ['blob']
+ ]
+ : [
+ ['list'],
+ ['html', { open: 'on-failure' }],
+ ],
+
+
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ headless: false,
+ trace: 'off', // we are manually handling tracing in _test.setup.ts
+ },
+
+ projects: [
+ {
+ name: 'e2e-electron',
+ use: {
+ web: false,
+ artifactDir: 'e2e-electron'
+ },
+
+ },
+ {
+ name: 'e2e-browser',
+ use: {
+ web: true,
+ artifactDir: 'e2e-browser',
+ headless: false,
+ },
+ grep: /@web/
+ },
+ ],
+});
diff --git a/scripts/convert-xunit-to-junit.sh b/scripts/convert-xunit-to-junit.sh
deleted file mode 100755
index 4603b90aac6..00000000000
--- a/scripts/convert-xunit-to-junit.sh
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/bin/bash
-
-# Check if a directory name was passed as a parameter
-if [ -z "$1" ]; then
- echo "No directory specified. Usage: sh ./scripts/convert-xunit-to-junit.sh "
- exit 1
-fi
-
-# Assign the directory parameter
-DIR_NAME="$1"
-
-# Input and output file paths, dynamically using the specified directory
-XUNIT_FILE="./.build/logs/$DIR_NAME/test-results/xunit-results.xml"
-CLEAN_XUNIT_FILE="./.build/logs/$DIR_NAME/test-results/xunit-results-clean.xml"
-JUNIT_FILE="./.build/logs/$DIR_NAME/test-results/results.xml"
-
-# Create the output directory if it doesn't exist
-OUTPUT_DIR=$(dirname "$JUNIT_FILE")
-mkdir -p "$OUTPUT_DIR"
-
-# Check if XUnit XML file exists
-if [ ! -f "$XUNIT_FILE" ]; then
- echo "XUnit file $XUNIT_FILE not found!"
- exit 1
-fi
-
-# Check if the XUnit XML file is empty
-if [ ! -s "$XUNIT_FILE" ]; then
- echo "Error: XUnit file $XUNIT_FILE exists but is empty!"
- exit 1
-fi
-
-# When we started logging stack traces in middle of test results ANSI escape codes were added to the XML file
-# These escape codes are not valid XML and cause xmllint to fail. So we need to strip them out.
-# - `` sequences represent ANSI escape codes in XML (used for colors and formatting).
-# - `\u001b` is the raw representation of the escape code in other formats.
-# Create a cleaned copy of the input file without escape sequences.
-sed -E 's/\[[0-9;]*[a-zA-Z]//g' "$XUNIT_FILE" | sed -E 's/\u001b\[[0-9;]*[a-zA-Z]//g' > "$CLEAN_XUNIT_FILE"
-
-# Validate the input XML file format before proceeding
-if ! /usr/bin/xmllint --noout "$CLEAN_XUNIT_FILE" 2>/dev/null; then
- echo "Error: $CLEAN_XUNIT_FILE is not a well-formed XML file."
- exit 1
-fi
-
-# Create a JUnit XML structure from XUnit
-echo '' > "$JUNIT_FILE"
-echo '' >> "$JUNIT_FILE"
-
-# Extract the entire block from the XUnit file
-TESTSUITE=$(/usr/bin/xmllint --xpath '//*[local-name()="testsuite"]' "$CLEAN_XUNIT_FILE" 2>/dev/null)
-
-# If no elements were found, output an error and exit
-if [ -z "$TESTSUITE" ]; then
- echo "Error: No elements found in the XUnit file."
- exit 1
-fi
-
-# Debug: Print the entire content
-# echo "Extracted content:"
-# echo "$TESTSUITE"
-
-# Extract the opening tag and its attributes
-TESTSUITE_OPENING=$(echo "$TESTSUITE" | sed -n 's/^\(]*>\).*/\1/p')
-
-# If a valid opening tag is found, proceed
-if [ -n "$TESTSUITE_OPENING" ]; then
- # Add the opening tag to the JUnit file
- echo "$TESTSUITE_OPENING" >> "$JUNIT_FILE"
-
- # Extract and add the elements for this
- # This will capture all the nested elements correctly
- TESTCASES=$(echo "$TESTSUITE" | xmllint --xpath '//*[local-name()="testcase"]' - 2>/dev/null)
-
- # Debug: Print the extracted elements
- # echo "Extracted testcases:"
- # echo "$TESTCASES"
-
- # Add the elements to the JUnit file
- if [ -n "$TESTCASES" ]; then
- echo "$TESTCASES" | sed 's###g' >> "$JUNIT_FILE"
- else
- echo "Warning: No elements found in this testsuite."
- fi
-
- # Close the tag after processing its test cases
- echo '' >> "$JUNIT_FILE"
-else
- echo "Error: Malformed testsuite element."
- exit 1
-fi
-
-# Close the root element
-echo '' >> "$JUNIT_FILE"
-
-# Detect if running on macOS (BSD) or Linux and adjust sed accordingly
-case "$OSTYPE" in
- darwin*)
- # macOS/BSD sed: use -i '' for in-place edits
- sed -i '' -e 's###g' "$JUNIT_FILE"
- ;;
- *)
- # Linux/GNU sed: use -i without ''
- sed -i 's###g' "$JUNIT_FILE"
- ;;
-esac
-
-echo "Conversion complete. JUnit XML saved to: $JUNIT_FILE"
diff --git a/scripts/slack-skipped-tests.js b/scripts/slack-skipped-tests.js
index 9604b58a9f9..d181904c671 100644
--- a/scripts/slack-skipped-tests.js
+++ b/scripts/slack-skipped-tests.js
@@ -8,7 +8,7 @@ const { execSync } = require('child_process');
const slackSkippedTests = (slackWebhookUrl) => {
try {
const skippedTests = execSync(
- `grep -r --include \\*.test.ts -E "describe\\.skip|it\\.skip" test/smoke/src/areas/positron | sed 's/\\.test\\.ts.*$/.test.ts/'`
+ `grep -r --include \\*.test.ts -E "describe\\.skip|test\\.skip" test/smoke/src/areas/positron | sed 's/\\.test\\.ts.*$/.test.ts/'`
).toString();
const slackMessage = {
diff --git a/test/automation/src/application.ts b/test/automation/src/application.ts
index 2a0084e1c1d..bdee7548886 100644
--- a/test/automation/src/application.ts
+++ b/test/automation/src/application.ts
@@ -104,9 +104,11 @@ export class Application {
await this._code?.startTracing(name);
}
- async stopTracing(name: string, persist: boolean): Promise {
- await this._code?.stopTracing(name, persist);
+ // --- Start Positron ---
+ async stopTracing(name: string, persist: boolean, customPath?: string): Promise {
+ await this._code?.stopTracing(name, persist, customPath);
}
+ // --- End Positron ---
private async startApplication(extraArgs: string[] = []): Promise {
const code = this._code = await launch({
diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts
index a925cdd65bc..5235a845a89 100644
--- a/test/automation/src/code.ts
+++ b/test/automation/src/code.ts
@@ -26,6 +26,9 @@ export interface LaunchOptions {
readonly remote?: boolean;
readonly web?: boolean;
readonly tracing?: boolean;
+ // --- Start Positron ---
+ readonly snapshots?: boolean;
+ // --- End Positron ---
readonly headless?: boolean;
readonly browser?: 'chromium' | 'webkit' | 'firefox';
}
@@ -121,9 +124,11 @@ export class Code {
return await this.driver.startTracing(name);
}
- async stopTracing(name: string, persist: boolean): Promise {
- return await this.driver.stopTracing(name, persist);
+ // --- Start Positron ---
+ async stopTracing(name: string, persist: boolean, customPath?: string): Promise {
+ return await this.driver.stopTracing(name, persist, customPath);
}
+ // --- End Positron ---
async dispatchKeybinding(keybinding: string): Promise {
await this.driver.dispatchKeybinding(keybinding);
diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts
index 5adaa33d7d8..9b455efc2a0 100644
--- a/test/automation/src/playwrightBrowser.ts
+++ b/test/automation/src/playwrightBrowser.ts
@@ -105,7 +105,9 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) {
if (tracing) {
try {
- await measureAndLog(() => context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger);
+ // --- Start Positron ---
+ await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots: options.snapshots, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger);
+ // --- End Positron ---
} catch (error) {
logger.log(`Playwright (Browser): Failed to start playwright tracing (${error})`); // do not fail the build when this fails
}
diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts
index 55783b1355b..a1f2eee49fd 100644
--- a/test/automation/src/playwrightDriver.ts
+++ b/test/automation/src/playwrightDriver.ts
@@ -57,7 +57,9 @@ export class PlaywrightDriver {
}
}
- async stopTracing(name: string, persist: boolean): Promise {
+ // --- Start Positron ---
+ async stopTracing(name: string, persist: boolean = true, customPath?: string): Promise {
+ // --- End Positron ---
if (!this.options.tracing) {
return; // tracing disabled
}
@@ -67,21 +69,10 @@ export class PlaywrightDriver {
if (persist) {
// --- Start Positron ---
// Positron: Windows has issues with long paths, shortened the name
- persistPath = join(this.options.logsPath, `trace-${PlaywrightDriver.traceCounter++}-${name.replace(/\s+/g, '-')}.zip`);
+ persistPath = customPath || join(this.options.logsPath, `trace-${PlaywrightDriver.traceCounter++}-${name.replace(/\s+/g, '-')}.zip`);
// --- End Positron ---
}
-
await measureAndLog(() => this.context.tracing.stopChunk({ path: persistPath }), `stopTracing for ${name}`, this.options.logger);
-
- // To ensure we have a screenshot at the end where
- // it failed, also trigger one explicitly. Tracing
- // does not guarantee to give us a screenshot unless
- // some driver action ran before.
- if (persist) {
- // --- Start Positron ---
- await this.takeScreenshot(`${name}`);
- // --- End Positron ---
- }
} catch (error) {
// Ignore
}
diff --git a/test/automation/src/playwrightElectron.ts b/test/automation/src/playwrightElectron.ts
index 660320be235..f2d7c1e5a81 100644
--- a/test/automation/src/playwrightElectron.ts
+++ b/test/automation/src/playwrightElectron.ts
@@ -45,7 +45,9 @@ async function launchElectron(configuration: IElectronConfiguration, options: La
if (tracing) {
try {
- await measureAndLog(() => context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger);
+ // --- Start Positron ---
+ await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots: options.snapshots /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger);
+ // --- End Positron ---
} catch (error) {
logger.log(`Playwright (Electron): Failed to start playwright tracing (${error})`); // do not fail the build when this fails
}
diff --git a/test/automation/src/positron/fixtures/positronPythonFixtures.ts b/test/automation/src/positron/fixtures/positronPythonFixtures.ts
index 2c51ec7708a..090c61e4449 100644
--- a/test/automation/src/positron/fixtures/positronPythonFixtures.ts
+++ b/test/automation/src/positron/fixtures/positronPythonFixtures.ts
@@ -17,11 +17,7 @@ export class PositronPythonFixtures {
static async SetupFixtures(app: Application, skipReadinessCheck: boolean = false) {
const fixtures = new PositronPythonFixtures(app);
- await expect(async () => {
- await fixtures.startPythonInterpreter(skipReadinessCheck);
- }).toPass({
- timeout: 60000
- });
+ await fixtures.startPythonInterpreter(skipReadinessCheck);
}
async startPythonInterpreter(skipReadinessCheck: boolean = false) {
@@ -36,7 +32,7 @@ export class PositronPythonFixtures {
await this.app.workbench.positronConsole.selectInterpreter(InterpreterType.Python, desiredPython, skipReadinessCheck);
await this.app.workbench.positronConsole.waitForReady('>>>', 2000);
} catch (e) {
- this.app.code.driver.takeScreenshot('startPythonInterpreter');
+ await this.app.code.driver.takeScreenshot('startPythonInterpreter');
throw e;
}
await this.app.workbench.positronConsole.logConsoleContents();
@@ -56,8 +52,7 @@ export class PositronPythonFixtures {
await this.app.workbench.positronPopups.installIPyKernel();
}
- await this.app.workbench.positronConsole.waitForReady('>>>', 2000);
-
+ await expect(this.app.workbench.positronConsole.activeConsole.getByText('>>>')).toBeVisible({ timeout: 30000 });
await this.app.workbench.positronConsole.logConsoleContents();
return interpreterInfo;
diff --git a/test/automation/src/positron/positronConsole.ts b/test/automation/src/positron/positronConsole.ts
index 5a78da34fbb..45cdd8bdbd3 100644
--- a/test/automation/src/positron/positronConsole.ts
+++ b/test/automation/src/positron/positronConsole.ts
@@ -36,7 +36,7 @@ export class PositronConsole {
consoleRestartButton: PositronBaseElement;
get activeConsole() {
- return this.code.driver.getLocator(ACTIVE_CONSOLE_INSTANCE);
+ return this.code.driver.page.locator(ACTIVE_CONSOLE_INSTANCE);
}
get emptyConsole() {
@@ -67,16 +67,15 @@ export class PositronConsole {
}
await this.quickaccess.runCommand(command, { keepOpen: true });
-
await this.quickinput.waitForQuickInputOpened();
await this.quickinput.type(desiredInterpreterString);
// Wait until the desired interpreter string appears in the list and select it.
// We need to click instead of using 'enter' because the Python select interpreter command
// may include additional items above the desired interpreter string.
- const interpreterElem = await this.quickinput.selectQuickInputElementContaining(desiredInterpreterString);
+ await this.quickinput.selectQuickInputElementContaining(desiredInterpreterString);
await this.quickinput.waitForQuickInputClosed();
- return interpreterElem;
+ return;
}
async selectAndGetInterpreter(
@@ -160,37 +159,29 @@ export class PositronConsole {
/**
* Check if the console is ready with Python or R, or if no interpreter is running.
- * @param retryCount The number of times to retry waiting for the console to be ready.
* @throws An error if the console is not ready after the retry count.
*/
- async waitForReadyOrNoInterpreter(retryCount: number = 800) {
- for (let i = 0; i < retryCount; i++) {
- // Check if the console is ready with Python.
- try {
- await this.waitForReady('>>>', 5);
- // The console is ready with Python.
- return;
- } catch (error) {
- // Python is not ready. Try the next interpreter.
- }
-
- // Check if the console is ready with R.
- try {
- await this.waitForReady('>', 5);
- // The console is ready with R.
- return;
- } catch (error) {
- // R is not ready. Try the next interpreter.
- }
-
- // Check if there is no interpreter running.
- try {
- await this.waitForNoInterpretersRunning(5);
- // The console is ready with no interpreter running.
- return;
- } catch (error) {
- // Text indicating no interpreter is running is not present. Try again.
- }
+ async waitForReadyOrNoInterpreter() {
+ const page = this.code.driver.page;
+
+ // ensure interpreter(s) containing starting/discovering do not exist in DOM
+ await expect(page.locator('text=/^Starting up|^Starting|^Discovering( \\w+)? interpreters|starting\\.$/i')).toHaveCount(0, { timeout: 30000 });
+
+ // ensure we are on Console tab
+ await page.getByRole('tab', { name: 'Console', exact: true }).locator('a').click();
+
+ // wait for the dropdown to contain R, Python, or No Interpreter.
+ const currentInterpreter = await page.locator('.top-action-bar-interpreters-manager').textContent() || '';
+
+ if (currentInterpreter.includes('Python')) {
+ await expect(page.getByRole('code').getByText('>>>')).toBeVisible({ timeout: 30000 });
+ return;
+ } else if (currentInterpreter.includes('R')) {
+ await expect(page.getByRole('code').getByText('>')).toBeVisible({ timeout: 30000 });
+ return;
+ } else if (currentInterpreter.includes('Start Interpreter')) {
+ await expect(page.getByText('There is no interpreter')).toBeVisible();
+ return;
}
// If we reach here, the console is not ready.
diff --git a/test/automation/src/positron/positronExplorer.ts b/test/automation/src/positron/positronExplorer.ts
index d9404d9d65e..daf78db16c9 100644
--- a/test/automation/src/positron/positronExplorer.ts
+++ b/test/automation/src/positron/positronExplorer.ts
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
+import { expect } from '@playwright/test';
import { Code } from '../code';
// import { QuickAccess } from '../quickaccess';
import { PositronTextElement } from './positronBaseElement';
@@ -17,6 +18,7 @@ const POSITRON_EXPLORER_PROJECT_FILES = 'div[id="workbench.view.explorer"] span[
*/
export class PositronExplorer {
explorerProjectTitle: PositronTextElement;
+ explorerProjectTitleLocator = this.code.driver.page.locator(POSITRON_EXPLORER_PROJECT_TITLE);
constructor(protected code: Code) {
this.explorerProjectTitle = new PositronTextElement(POSITRON_EXPLORER_PROJECT_TITLE, this.code);
@@ -24,7 +26,7 @@ export class PositronExplorer {
/**
* Constructs a string array of the top-level project files/directories in the explorer.
- * @param {string} locator - The locator for the project files/directories in the explorer.
+ * @param locator - The locator for the project files/directories in the explorer.
* @returns Promise Array of strings representing the top-level project files/directories in the explorer.
*/
async getExplorerProjectFiles(locator: string = POSITRON_EXPLORER_PROJECT_FILES): Promise {
@@ -39,6 +41,6 @@ export class PositronExplorer {
async waitForProjectFileToAppear(filename: string) {
const escapedFilename = filename.replace(/\./g, '\\.').toLowerCase();
- await this.code.waitForElement(`.${escapedFilename}-name-file-icon`);
+ await expect(this.code.driver.page.locator(`.${escapedFilename}-name-file-icon`)).toBeVisible({ timeout: 30000 });
}
}
diff --git a/test/automation/src/positron/positronInterpreterDropdown.ts b/test/automation/src/positron/positronInterpreterDropdown.ts
index aa8324fa2b6..ceba6d1c43f 100644
--- a/test/automation/src/positron/positronInterpreterDropdown.ts
+++ b/test/automation/src/positron/positronInterpreterDropdown.ts
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
-import { Locator } from '@playwright/test';
+import { expect, Locator } from '@playwright/test';
import { Code } from '../code';
import { getInterpreterType, InterpreterInfo, InterpreterType } from './utils/positronInterpreterInfo';
@@ -60,19 +60,16 @@ export class PositronInterpreterDropdown {
*/
private async getPrimaryInterpreter(description: string | InterpreterType) {
// Wait for the primary interpreters to load
- await this.code.waitForElements('.primary-interpreter', false);
- const allPrimaryInterpreters = await this.interpreterGroups
- .locator('.primary-interpreter')
- .all();
- if (allPrimaryInterpreters.length === 0) {
- this.code.logger.log('Failed to locate primary interpreters');
- return undefined;
- }
+ expect(await this.code.driver.page.locator('.primary-interpreter').count()).toBeGreaterThan(0);
+ this.code.logger.log('Primary interpreters loaded: ', this.code.driver.page.locator('.primary-interpreter').count());
// Look for a primary interpreter that matches the provided description
+ const allPrimaryInterpreters = await this.code.driver.page.locator('.primary-interpreter').all();
+
for (const interpreter of allPrimaryInterpreters) {
// Try to match on interpreter name
const interpreterName = await this.getInterpreterName(interpreter);
+
if (!interpreterName) {
// Shouldn't happen, but if it does, proceed to the next interpreter
continue;
diff --git a/test/automation/src/positron/positronNotebooks.ts b/test/automation/src/positron/positronNotebooks.ts
index 33481ebfc8e..7a9b4cf32cd 100644
--- a/test/automation/src/positron/positronNotebooks.ts
+++ b/test/automation/src/positron/positronNotebooks.ts
@@ -29,45 +29,31 @@ const ACTIVE_ROW_SELECTOR = `.notebook-editor .monaco-list-row.focused`;
* Reuseable Positron notebook functionality for tests to leverage. Includes selecting the notebook's interpreter.
*/
export class PositronNotebooks {
- kernelLabel = this.code.driver.getLocator(KERNEL_LABEL);
+ kernelLabel = this.code.driver.page.locator(KERNEL_LABEL);
frameLocator = this.code.driver.page.frameLocator(OUTER_FRAME).frameLocator(INNER_FRAME);
+ notebookProgressBar = this.code.driver.page.locator('[id="workbench\\.parts\\.editor"]').getByRole('progressbar');
+
constructor(private code: Code, private quickinput: QuickInput, private quickaccess: QuickAccess, private notebook: Notebook) { }
async selectInterpreter(kernelGroup: string, desiredKernel: string) {
+ await expect(this.notebookProgressBar).not.toBeVisible({ timeout: 30000 });
+ await expect(this.code.driver.page.locator(DETECTING_KERNELS_TEXT)).not.toBeVisible({ timeout: 30000 });
- // get the kernel label text
- let interpreterManagerText = (await this.code.waitForElement(KERNEL_LABEL)).textContent;
+ // Wait for either "select kernel" or "the desired kernel" to appear in KERNEL_LABEL
+ const kernelRegex = new RegExp(`${SELECT_KERNEL_TEXT}|${desiredKernel}`);
+ const kernelLabelLocator = this.code.driver.page.locator(KERNEL_LABEL);
+ await expect(kernelLabelLocator).toHaveText(kernelRegex, { timeout: 10000 });
- // if we are still detecting kernels, wait extra time for the correct kernel or for the
- // "Select Kernel" option to appear
- if (interpreterManagerText === DETECTING_KERNELS_TEXT) {
- interpreterManagerText = (await this.code.waitForElement(KERNEL_LABEL, (e) =>
- e!.textContent.includes(desiredKernel) ||
- e!.textContent.includes(SELECT_KERNEL_TEXT), 600)).textContent;
- }
+ // Retrieve the matched text for conditional logic
+ const matchedText = await kernelLabelLocator.textContent() || '';
- // if select kernel appears, select the proper kernel
- // also if the wrong kernel has shown up, select the proper kernel
- if (interpreterManagerText === SELECT_KERNEL_TEXT || !interpreterManagerText.includes(desiredKernel)) {
- await this.code.waitAndClick(KERNEL_ACTION);
+ if (!new RegExp(desiredKernel).test(matchedText)) {
+ await this.code.driver.page.locator(KERNEL_ACTION).click();
await this.quickinput.waitForQuickInputOpened();
- // depending on random timing, it may or may not be necessary to select the kernel group
- try {
- await this.quickinput.selectQuickInputElementContaining(kernelGroup);
- } catch {
- this.code.logger.log('Kernel group not found');
- }
-
- // Close dialog if quick input element can't found and try to proceed
- // (this may be necessary if the interpreter was set while this block was running)
- try {
- await this.quickinput.selectQuickInputElementContaining(desiredKernel);
- await this.quickinput.waitForQuickInputClosed();
- } catch {
- this.code.logger.log('Closing quick input');
- await this.code.driver.getKeyboard().press('Escape');
- }
+ await this.quickinput.selectQuickInputElementContaining(kernelGroup);
+ await this.quickinput.selectQuickInputElementContaining(desiredKernel);
+ await this.quickinput.waitForQuickInputClosed();
}
}
diff --git a/test/automation/src/positron/positronOutput.ts b/test/automation/src/positron/positronOutput.ts
index 3d01d6699ad..fea3b556414 100644
--- a/test/automation/src/positron/positronOutput.ts
+++ b/test/automation/src/positron/positronOutput.ts
@@ -27,8 +27,12 @@ export class PositronOutput {
await this.quickinput.waitForQuickInputClosed();
}
+ async clickOutputTab() {
+ await this.code.driver.page.getByRole('tab', { name: 'Output' }).locator('a').click();
+ }
+
async waitForOutContaining(fragment: string) {
- const outputLine = this.code.driver.getLocator(OUTPUT_LINE);
+ const outputLine = this.code.driver.page.locator(OUTPUT_LINE);
await outputLine.getByText(fragment).first().isVisible();
}
}
diff --git a/test/automation/src/positron/positronPlots.ts b/test/automation/src/positron/positronPlots.ts
index ecb449455cf..51147197fac 100644
--- a/test/automation/src/positron/positronPlots.ts
+++ b/test/automation/src/positron/positronPlots.ts
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
-import { Locator } from '@playwright/test';
+import { expect, Locator } from '@playwright/test';
import { Code } from '../code';
const CURRENT_PLOT = '.plot-instance img';
@@ -47,11 +47,11 @@ export class PositronPlots {
}
async waitForCurrentPlot() {
- await this.code.waitForElement(CURRENT_PLOT);
+ await expect(this.code.driver.page.locator(CURRENT_PLOT)).toBeVisible({ timeout: 30000 });
}
async waitForCurrentStaticPlot() {
- await this.code.waitForElement(CURRENT_STATIC_PLOT);
+ await expect(this.code.driver.page.locator(CURRENT_STATIC_PLOT)).toBeVisible({ timeout: 30000 });
}
getWebviewPlotLocator(selector: string): Locator {
@@ -63,20 +63,25 @@ export class PositronPlots {
}
async waitForWebviewPlot(selector: string, state: 'attached' | 'visible' = 'visible', RWeb = false) {
+ const locator = RWeb ? this.getRWebWebviewPlotLocator(selector) : this.getWebviewPlotLocator(selector);
- if (RWeb) {
- await this.getRWebWebviewPlotLocator(selector).waitFor({ state, timeout: 30000 });
+ if (state === 'attached') {
+ await expect(locator).toBeAttached({ timeout: 30000 });
} else {
- await this.getWebviewPlotLocator(selector).waitFor({ state, timeout: 30000 });
+ await expect(locator).toBeVisible({ timeout: 30000 });
}
}
async clearPlots() {
- await this.code.waitAndClick(CLEAR_PLOTS);
+ const clearPlotsButton = this.code.driver.page.locator(CLEAR_PLOTS);
+
+ if (await clearPlotsButton.isVisible() && await clearPlotsButton.isEnabled()) {
+ await clearPlotsButton.click();
+ }
}
async waitForNoPlots() {
- await this.code.waitForElement(CURRENT_PLOT, (result) => !result);
+ await expect(this.code.driver.page.locator(CURRENT_PLOT)).not.toBeVisible();
}
async getCurrentPlotAsBuffer(): Promise {
diff --git a/test/automation/src/positron/positronTerminal.ts b/test/automation/src/positron/positronTerminal.ts
index 7718e413fe7..99f2b9be2dd 100644
--- a/test/automation/src/positron/positronTerminal.ts
+++ b/test/automation/src/positron/positronTerminal.ts
@@ -3,21 +3,22 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
+import { Locator } from '@playwright/test';
import { Code } from '../code';
-import { Selector } from '../terminal';
-
-const TERMINAL_TAB = 'a[aria-label="Terminal (⌃`)"]';
export class PositronTerminal {
+ terminalTab: Locator;
- constructor(private code: Code) { }
+ constructor(private code: Code) {
+ this.terminalTab = this.code.driver.page.getByRole('tab', { name: 'Terminal' }).locator('a');
+ }
async sendKeysToTerminal(key: string) {
- await this.code.waitAndClick(Selector.TerminalView);
await this.code.driver.getKeyboard().press(key);
}
async clickTerminalTab() {
- await this.code.waitAndClick(TERMINAL_TAB);
+ await this.clickTerminalTab();
+ await this.terminalTab.click();
}
}
diff --git a/test/automation/src/positron/positronViewer.ts b/test/automation/src/positron/positronViewer.ts
index c00142ff640..4464ccdd7ad 100644
--- a/test/automation/src/positron/positronViewer.ts
+++ b/test/automation/src/positron/positronViewer.ts
@@ -14,7 +14,7 @@ const FULL_APP = 'body';
export class PositronViewer {
- fullApp = this.code.driver.getLocator(FULL_APP);
+ fullApp = this.code.driver.page.locator(FULL_APP);
viewerFrame = this.code.driver.page.frameLocator(OUTER_FRAME).frameLocator(INNER_FRAME);
constructor(private code: Code) { }
@@ -28,10 +28,11 @@ export class PositronViewer {
}
async refreshViewer() {
- await this.code.waitAndClick(REFRESH_BUTTON);
+ await this.code.driver.page.locator(REFRESH_BUTTON).click({ timeout: 15000 });
}
async clearViewer() {
+ await this.code.driver.page.getByRole('tab', { name: 'Viewer' }).locator('a').click();
await this.fullApp.getByLabel(/Clear the/).click();
}
}
diff --git a/test/automation/src/quickinput.ts b/test/automation/src/quickinput.ts
index 65a612cd363..65eecc7bccc 100644
--- a/test/automation/src/quickinput.ts
+++ b/test/automation/src/quickinput.ts
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { Code } from './code';
-import { IElement } from './driver';
export class QuickInput {
@@ -62,23 +61,12 @@ export class QuickInput {
}
}
// --- Start Positron ---
- async selectQuickInputElementContaining(contains: string): Promise {
- const selector = `${QuickInput.QUICK_INPUT_ROW}[aria-label*="${contains}"]`;
- try {
- const element = this.code.getElement(selector);
- await this.code.waitAndClick(selector);
- return element;
- } catch (ex) {
- // Show a more helpful error message by clearing the input and logging the list of items
- await this.type('');
- const elements = await this.code.waitForElements(QuickInput.QUICK_INPUT_ROW, false);
- const ariaLabels = elements.map(e => e.attributes['aria-label']);
- throw new Error(`Could not find item containing '${contains}' in list:\n${ariaLabels.join('\n')}`);
- }
+ async selectQuickInputElementContaining(text: string): Promise {
+ await this.code.driver.page.locator(`${QuickInput.QUICK_INPUT_ROW}[aria-label*="${text}"]`).first().click({ timeout: 10000 });
}
async clickOkOnQuickInput(): Promise {
- await this.code.driver.getLocator(QuickInput.QUICKINPUT_OK_BUTTON).click();
+ await this.code.driver.page.locator(QuickInput.QUICKINPUT_OK_BUTTON).click();
}
// --- End Positron ---
}
diff --git a/test/smoke/README.md b/test/smoke/README.md
index 43809fc9837..34580f98519 100644
--- a/test/smoke/README.md
+++ b/test/smoke/README.md
@@ -169,7 +169,7 @@ Make sure that you have followed the [Machine Setup](https://connect.posit.it/po
### Test Dependencies
-Several tests use [QA Content Examples](https://github.com/posit-dev/qa-example-content). You will need to install the dependencies for those projects. A few current tests also use additional packages. You can look in the [positron-full-test.yml](https://github.com/posit-dev/positron/blob/39a01b71064e2ef3ef5822c95691a034b7e0194f/.github/workflows/positron-full-test.yml) Github action for the full list.
+Several tests use [QA Content Examples](https://github.com/posit-dev/qa-example-content). You will need to install the dependencies for those projects. A few current tests also use additional packages. You can look in the [positron-full-test.yml](https://github.com/posit-dev/positron/blob/39a01b71064e2ef3ef5822c95691a034b7e0194f/.github/workflows/positron-full-test.yml) Github action for the full list.
## Running Tests
@@ -237,9 +237,9 @@ yarn smoketest-pr --build /Applications/Positron.app --parallel --jobs 3
## Test Project
-Before any of the tests start executing the test framework clones down the [QA Content Examples](https://github.com/posit-dev/qa-example-content) repo. This repo contains R and Python files that are run by the automated tests and also includes data files (such as Excel, SQLite, & parquet) that support the test scripts. If you make additions to QA Content Examples for a test, please be sure that the data files are free to use in a public repository.
+Before any of the tests start executing the test framework clones down the [QA Content Examples](https://github.com/posit-dev/qa-example-content) repo. This repo contains R and Python files that are run by the automated tests and also includes data files (such as Excel, SQLite, & parquet) that support the test scripts. If you make additions to QA Content Examples for a test, please be sure that the data files are free to use in a public repository.
-For Python, add any package requirements to the `requirements.txt` file in the root of the [QA Content Examples](https://github.com/posit-dev/qa-example-content) repo. We generally do NOT pin them to a specific version, as test can be run against different versions of python and conflicts could arise. If this becomes a problem, we can revisit this mechanism.
+For Python, add any package requirements to the `requirements.txt` file in the root of the [QA Content Examples](https://github.com/posit-dev/qa-example-content) repo. We generally do NOT pin them to a specific version, as test can be run against different versions of python and conflicts could arise. If this becomes a problem, we can revisit this mechanism.
For R, add any package requirements to the "imports" section of the `DESCRIPTION` file in the root of the [QA Content Examples](https://github.com/posit-dev/qa-example-content) repo.
@@ -264,26 +264,26 @@ await app.workbench.quickaccess.runCommand('workbench.action.toggleDevTools');`
### Playwright Traces
-Note that in launch.json for `Launch Smoke Test` we are passing the `--tracing` argument for you. This will result in Playwright traces being generated locally for you when tests fail at `.build/logs/smoke-tests-electron/{testCase}`. Note that for command line runs you will need to pass this arg yourself to get the trace file(s).
+Note that in launch.json for `Launch Smoke Test` we are passing the `--tracing` argument for you. This will result in Playwright traces being generated locally for you when tests fail at `.build/logs/smoke-tests-electron/{testCase}`. Note that for command line runs you will need to pass this arg yourself to get the trace file(s).
## Running Tests in Github Actions
-New tests are not complete until they run successfully across operating systems (Mac, Windows, & Ubuntu) and in [Github Actions](https://github.com/posit-dev/positron/actions/workflows/positron-full-test.yml). In Github Actions we use an Ubuntu instance to run the tests, so if you are developing your tests using a Mac or on Windows, this is an opportunity to test a different operating system. Also, you can easily run your new tests against a branch to verify them before merge. Simply pick the branch after you click on "Run Workflow". Note that you can also temporarily modify the workflow itself to get your new tests executed more quickly. To do this, skip the runs of the unit and integration tests.
+New tests are not complete until they run successfully across operating systems (Mac, Windows, & Ubuntu) and in [Github Actions](https://github.com/posit-dev/positron/actions/workflows/positron-full-test.yml). In Github Actions we use an Ubuntu instance to run the tests, so if you are developing your tests using a Mac or on Windows, this is an opportunity to test a different operating system. Also, you can easily run your new tests against a branch to verify them before merge. Simply pick the branch after you click on "Run Workflow". Note that you can also temporarily modify the workflow itself to get your new tests executed more quickly. To do this, skip the runs of the unit and integration tests.
### Github Actions Test Artifacts
-When a run is complete, you can debug any test failures that occurred using the uploaded run artifacts. The artifacts are available as a ZIP file from inside the workflow run. Each artifact zip contains: a folder for each test file and an overall run log. Inside the folder corresponding to each test file, you will find zip files that are Playwright traces. Note that the trace files are only present for failed cases.
+When a run is complete, you can debug any test failures that occurred using the uploaded run artifacts. The artifacts are available as a ZIP file from inside the workflow run. Each artifact zip contains: a folder for each test file and an overall run log. Inside the folder corresponding to each test file, you will find zip files that are Playwright traces. Note that the trace files are only present for failed cases.
-Playwright traces can be drag and dropped to the [Trace Viewer](https://trace.playwright.dev/). The trace will usually give you a good visualization of the failed test, but they can be sparse on details. More details are available from the run log (smoke-test-runner.log). It has a start and end marker for each test case.
+Playwright traces can be drag and dropped to the [Trace Viewer](https://trace.playwright.dev/). The trace will usually give you a good visualization of the failed test, but they can be sparse on details. More details are available from the run log (e2e-test-runner.log). It has a start and end marker for each test case.
## Notes About Updating Specific Tests
### Plot Tests That Use Resemblejs
-In order to get the "golden screenshots" used for plot comparison is CI, you will need to temporarily uncomment the line of code marked with `capture master image in CI` or add a similar line of code for a new case. We must use CI taken snapshots because if the "golden screenshots" are taken locally, they will differ too much from the CI images to be useable with a proper threshold. You can't compare the current runtime plot against a snapshot until you have established a baseline screenshot from CI that is saved to `test/smoke/plots`.
+In order to get the "golden screenshots" used for plot comparison is CI, you will need to temporarily uncomment the line of code marked with `capture master image in CI` or add a similar line of code for a new case. We must use CI taken snapshots because if the "golden screenshots" are taken locally, they will differ too much from the CI images to be useable with a proper threshold. You can't compare the current runtime plot against a snapshot until you have established a baseline screenshot from CI that is saved to `test/smoke/plots`.
## Tests run on PRs
-If you think your test should be run when PRs are created, add the string `#pr` to its name. The existing #pr cases were selected to give good overall coverage while keeping the overall execution time down to ten minutes or less. If your new test functionality covers a part of the application that no other tests cover, it is probably a good idea to include it in the #pr set.
+If you think your test should be run when PRs are created, add the string `#pr` to its name. The existing #pr cases were selected to give good overall coverage while keeping the overall execution time down to ten minutes or less. If your new test functionality covers a part of the application that no other tests cover, it is probably a good idea to include it in the #pr set.
diff --git a/test/smoke/package.json b/test/smoke/package.json
index 9ed74b9ccfd..9d325fbbaba 100644
--- a/test/smoke/package.json
+++ b/test/smoke/package.json
@@ -26,6 +26,7 @@
"@types/node-fetch": "^2.5.10",
"@types/resemblejs": "4.1.3",
"@types/rimraf": "3.0.2",
+ "archiver": "^7.0.1",
"npm-run-all": "^4.1.5",
"watch": "^1.0.2"
}
diff --git a/test/smoke/run-tests.js b/test/smoke/run-tests.js
deleted file mode 100644
index 4f4da5ea204..00000000000
--- a/test/smoke/run-tests.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-//@ts-check
-'use strict';
-
-require('./out/test-runner/config'); // must import first to set up the env vars
-const minimist = require('minimist');
-const { prepareTestEnv, cloneTestRepo, runMochaTests } = require('./out/test-runner');
-const OPTS = minimist(process.argv.slice(2));
-
-(async function main() {
- prepareTestEnv();
- cloneTestRepo();
- await runMochaTests(OPTS);
-})();
diff --git a/test/smoke/src/areas/positron/_global.setup.ts b/test/smoke/src/areas/positron/_global.setup.ts
new file mode 100644
index 00000000000..1de70636fdf
--- /dev/null
+++ b/test/smoke/src/areas/positron/_global.setup.ts
@@ -0,0 +1,22 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import * as os from 'os';
+import * as fs from 'fs';
+import { cloneTestRepo, prepareTestEnv } from '../../test-runner';
+
+const ROOT_PATH = process.cwd();
+const LOGS_ROOT_PATH = join(ROOT_PATH, 'test-logs');
+const TEST_DATA_PATH = join(os.tmpdir(), 'vscsmoke');
+const WORKSPACE_PATH = join(TEST_DATA_PATH, 'qa-example-content');
+
+async function globalSetup() {
+ fs.rmSync(LOGS_ROOT_PATH, { recursive: true, force: true });
+ prepareTestEnv(ROOT_PATH);
+ cloneTestRepo(WORKSPACE_PATH);
+}
+
+export default globalSetup;
diff --git a/test/smoke/src/areas/positron/_test.setup.ts b/test/smoke/src/areas/positron/_test.setup.ts
new file mode 100644
index 00000000000..7ab5eca242f
--- /dev/null
+++ b/test/smoke/src/areas/positron/_test.setup.ts
@@ -0,0 +1,312 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// Playwright and testing imports
+import * as playwright from '@playwright/test';
+const { test: base, expect: playwrightExpect } = playwright;
+
+// Node.js built-in modules
+import { join } from 'path';
+import * as os from 'os';
+import * as fs from 'fs';
+
+import path = require('path');
+// eslint-disable-next-line local/code-import-patterns
+import { rename, rm, access, mkdir } from 'fs/promises';
+import { constants } from 'fs';
+
+// Third-party packages
+import { randomUUID } from 'crypto';
+import archiver from 'archiver';
+
+// Local imports
+import { createLogger } from '../../test-runner/logger';
+import { Application, Logger, PositronPythonFixtures, PositronRFixtures } from '../../../../automation';
+import { createApp } from '../../utils';
+
+const TEMP_DIR = `temp-${randomUUID()}`;
+const ROOT_PATH = process.cwd();
+const LOGS_ROOT_PATH = join(ROOT_PATH, 'test-logs');
+let SPEC_NAME = '';
+
+export const test = base.extend({
+ suiteId: ['', { scope: 'worker', option: true }],
+
+ snapshots: [true, { scope: 'worker', auto: true }],
+
+ logsPath: [async ({ }, use, workerInfo) => {
+ const project = workerInfo.project.use as CustomTestOptions;
+ const logsPath = join(LOGS_ROOT_PATH, project.artifactDir, TEMP_DIR);
+ await use(logsPath);
+ }, { scope: 'worker', auto: true }],
+
+ logger: [async ({ logsPath }, use) => {
+ const logger = createLogger(logsPath);
+ await use(logger);
+ }, { auto: true, scope: 'worker' }],
+
+ options: [async ({ logsPath, logger, snapshots }, use, workerInfo) => {
+ const project = workerInfo.project.use as CustomTestOptions;
+ const TEST_DATA_PATH = join(os.tmpdir(), 'vscsmoke');
+ const EXTENSIONS_PATH = join(TEST_DATA_PATH, 'extensions-dir');
+ const WORKSPACE_PATH = join(TEST_DATA_PATH, 'qa-example-content');
+ const SPEC_CRASHES_PATH = join(ROOT_PATH, '.build', 'crashes', project.artifactDir, TEMP_DIR);
+
+ const options = {
+ codePath: process.env.BUILD,
+ workspacePath: WORKSPACE_PATH,
+ userDataDir: join(TEST_DATA_PATH, 'd'),
+ extensionsPath: EXTENSIONS_PATH,
+ logger,
+ logsPath,
+ crashesPath: SPEC_CRASHES_PATH,
+ verbose: process.env.VERBOSE,
+ remote: process.env.REMOTE,
+ web: project.web,
+ headless: project.headless,
+ tracing: true,
+ snapshots,
+ };
+
+ await use(options);
+ }, { scope: 'worker', auto: true }],
+
+ restartApp: [async ({ app }, use) => {
+ await app.restart();
+ await use(app);
+ }, { scope: 'test', timeout: 60000 }],
+
+ app: [async ({ options, logsPath, logger }, use) => {
+ const app = createApp(options);
+ await app.start();
+
+ await use(app);
+
+ await app.stop();
+
+ // rename the temp logs dir to the spec name
+ const specLogsPath = path.join(path.dirname(logsPath), SPEC_NAME);
+ await moveAndOverwrite(logsPath, specLogsPath);
+ }, { scope: 'worker', auto: true, timeout: 60000 }],
+
+ interpreter: [async ({ app, page }, use) => {
+ const setInterpreter = async (interpreterName: 'Python' | 'R') => {
+ const currentInterpreter = await page.locator('.top-action-bar-interpreters-manager').textContent() || '';
+
+ if (!currentInterpreter.includes(interpreterName)) {
+ if (interpreterName === 'Python') {
+ await PositronPythonFixtures.SetupFixtures(app, false);
+ } else if (interpreterName === 'R') {
+ await PositronRFixtures.SetupFixtures(app, false);
+ }
+ }
+ };
+
+ await use({ set: setInterpreter });
+ }, { scope: 'test', }],
+
+ r: [
+ async ({ interpreter }, use) => {
+ await interpreter.set('R');
+ await use();
+ },
+ { scope: 'test' }
+ ],
+
+ python: [
+ async ({ interpreter }, use) => {
+ await interpreter.set('Python');
+ await use();
+ },
+ { scope: 'test' }],
+
+ devTools: [async ({ app }, use) => {
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleDevTools');
+ await use();
+ },
+ { scope: 'test' }],
+
+ attachScreenshotsToReport: [async ({ app }, use, testInfo) => {
+ let screenShotCounter = 1;
+ const page = app.code.driver.page;
+ const screenshots: string[] = [];
+
+ app.code.driver.takeScreenshot = async function (name: string) {
+ const screenshotPath = testInfo.outputPath(`${screenShotCounter++}-${name}.png`);
+ await page.screenshot({ path: screenshotPath });
+ screenshots.push(screenshotPath);
+ };
+
+ await use();
+
+ // if test failed, take and attach screenshot
+ if (testInfo.status !== testInfo.expectedStatus) {
+ const screenshot = await page.screenshot();
+ await testInfo.attach('on-test-end', { body: screenshot, contentType: 'image/png' });
+ }
+
+ for (const screenshotPath of screenshots) {
+ testInfo.attachments.push({ name: path.basename(screenshotPath), path: screenshotPath, contentType: 'image/png' });
+ }
+
+ }, { auto: true }],
+
+ attachLogsToReport: [async ({ suiteId, logsPath }, use, testInfo) => {
+ await use();
+
+ if (!suiteId) { return; }
+
+ const zipPath = path.join(logsPath, 'logs.zip');
+ const output = fs.createWriteStream(zipPath);
+ const archive = archiver('zip', { zlib: { level: 9 } });
+
+ archive.on('error', (err) => {
+ throw err;
+ });
+
+ archive.pipe(output);
+
+ // add all log files to the archive
+ archive.glob('**/*', { cwd: logsPath, ignore: ['logs.zip'] });
+
+ // wait for the archive to finalize and the output stream to close
+ await new Promise((resolve, reject) => {
+ output.on('close', resolve);
+ output.on('error', reject);
+ archive.finalize();
+ });
+
+ // attach the zipped file to the report
+ await testInfo.attach(`logs-${path.basename(testInfo.file)}.zip`, {
+ path: zipPath,
+ contentType: 'application/zip',
+ });
+
+ // remove the logs.zip file
+ try {
+ await fs.promises.unlink(zipPath);
+ } catch (err) {
+ console.error(`Failed to remove ${zipPath}:`, err);
+ }
+ }, { auto: true }],
+
+ tracing: [async ({ app }, use, testInfo) => {
+ // start tracing
+ await app.startTracing();
+
+ await use(app);
+
+ // stop tracing
+ const title = path.basename(`_trace`); // do NOT use title of 'trace' - conflicts with the default trace
+ const tracePath = testInfo.outputPath(`${title}.zip`);
+ await app.stopTracing(title, true, tracePath);
+
+ // attach the trace to the report if CI and test failed or not in CI
+ const isCI = process.env.CI === 'true';
+ if (!isCI || testInfo.status !== testInfo.expectedStatus || testInfo.retry) {
+ testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
+ }
+
+ }, { auto: true, scope: 'test' }],
+
+ page: async ({ app }, use) => {
+ await use(app.code.driver.page);
+ },
+
+ autoTestFixture: [async ({ logger, suiteId }, use, testInfo) => {
+ if (!suiteId) { throw new Error('suiteId is required'); }
+
+ logger.log('');
+ logger.log(`>>> Test start: '${testInfo.title ?? 'unknown'}' <<<`);
+ logger.log('');
+
+ await use();
+
+ const failed = testInfo.status !== testInfo.expectedStatus;
+ const testTitle = testInfo.title;
+ const endLog = failed ? `>>> !!! FAILURE !!! Test end: '${testTitle}' !!! FAILURE !!! <<<` : `>>> Test end: '${testTitle}' <<<`;
+
+ logger.log('');
+ logger.log(endLog);
+ logger.log('');
+ }, { scope: 'test', auto: true }],
+});
+
+// Runs once per worker. If a worker handles multiple specs, these hooks only run for the first spec.
+// However, we are using `suiteId` to ensure each suite gets a new worker (and a fresh app
+// instance). This also ensures these before/afterAll hooks will run for EACH spec
+test.beforeAll(async ({ logger }, testInfo) => {
+ // since the worker doesn't know or have access to the spec name when it starts,
+ // we store the spec name in a global variable. this ensures logs are written
+ // to the correct folder even when the app is scoped to "worker".
+ // by storing the spec name globally, we can rename the logs folder after the suite finishes.
+ // note: workers are intentionally restarted per spec to scope logs by spec
+ // and provide a fresh app instance for each spec.
+ SPEC_NAME = testInfo.titlePath[0];
+ logger.log('');
+ logger.log(`>>> Suite start: '${testInfo.titlePath[0] ?? 'unknown'}' <<<`);
+ logger.log('');
+});
+
+test.afterAll(async function ({ logger }, testInfo) {
+ logger.log('');
+ logger.log(`>>> Suite end: '${testInfo.titlePath[0] ?? 'unknown'}' <<<`);
+ logger.log('');
+});
+
+export { playwrightExpect as expect };
+
+async function moveAndOverwrite(sourcePath, destinationPath) {
+ try {
+ await access(sourcePath, constants.F_OK);
+ } catch {
+ console.error(`moveAndOverwrite: source path does not exist: ${sourcePath}`);
+ return;
+ }
+
+ // check if the destination exists and delete it if so
+ try {
+ await access(destinationPath, constants.F_OK);
+ await rm(destinationPath, { recursive: true, force: true });
+ } catch (err) { }
+
+ // ensure parent directory of destination path exists
+ const destinationDir = path.dirname(destinationPath);
+ await mkdir(destinationDir, { recursive: true });
+
+ // rename source to destination
+ try {
+ await rename(sourcePath, destinationPath);
+ } catch (err) { }
+}
+
+interface TestFixtures {
+ restartApp: Application;
+ tracing: any;
+ page: playwright.Page;
+ attachScreenshotsToReport: any;
+ attachLogsToReport: any;
+ interpreter: { set: (interpreterName: 'Python' | 'R') => Promise };
+ r: void;
+ python: void;
+ autoTestFixture: any;
+ devTools: void;
+}
+
+interface WorkerFixtures {
+ suiteId: string;
+ snapshots: boolean;
+ artifactDir: string;
+ options: any;
+ app: Application;
+ logsPath: string;
+ logger: Logger;
+}
+
+export type CustomTestOptions = playwright.PlaywrightTestOptions & {
+ web: boolean;
+ artifactDir: string;
+ headless?: boolean;
+};
diff --git a/test/smoke/src/areas/positron/apps/python-apps.test.ts b/test/smoke/src/areas/positron/apps/python-apps.test.ts
index c8984fe5181..9b7bbd39c44 100644
--- a/test/smoke/src/areas/positron/apps/python-apps.test.ts
+++ b/test/smoke/src/areas/positron/apps/python-apps.test.ts
@@ -3,113 +3,91 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
import { join } from 'path';
-describe('Python Applications #pr', () => {
- setupAndStartApp();
-
- describe('Python Applications', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- afterEach(async function () {
- await this.app.workbench.quickaccess.runCommand('workbench.action.terminal.focus');
- await this.app.workbench.positronTerminal.sendKeysToTerminal('Control+C');
-
- // unreliable on ubuntu:
- // await this.app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('^C')));
-
- await this.app.workbench.positronViewer.refreshViewer();
- });
-
- it('Python - Verify Basic Dash App [C903305] #win', async function () {
- this.retries(1);
- const app = this.app as Application;
- const viewer = app.workbench.positronViewer;
-
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'dash_example', 'dash_example.py'));
- await app.workbench.positronEditor.pressPlay();
- await expect(viewer.getViewerFrame().getByText('Hello World')).toBeVisible({ timeout: 30000 });
- });
+test.use({
+ suiteId: __filename
+});
- // Skipped on windows due to https://github.com/posit-dev/positron/issues/5312
- it('Python - Verify Basic FastAPI App [C903306]', async function () {
- const app = this.app as Application;
- const viewer = app.workbench.positronViewer;
+test.describe('Python Applications', { tag: ['@pr'] }, () => {
+ test.afterEach(async function ({ app }) {
+ await app.workbench.quickaccess.runCommand('workbench.action.terminal.focus');
+ await app.workbench.positronTerminal.sendKeysToTerminal('Control+C');
+ // unreliable on ubuntu:
+ // await this.app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('^C')));
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ await app.workbench.positronViewer.clearViewer();
+ });
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'fastapi_example', 'fastapi_example.py'));
- await app.workbench.positronEditor.pressPlay();
- await expect(viewer.getViewerFrame().getByText('FastAPI')).toBeVisible({ timeout: 30000 });
- });
+ test('Python - Verify Basic Dash App [C903305]', { tag: ['@win'] }, async function ({ app, python }) {
+ const viewer = app.workbench.positronViewer;
- it('Python - Verify Basic Gradio App [C903307] #win', async function () {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'dash_example', 'dash_example.py'));
+ await app.workbench.positronEditor.pressPlay();
+ await expect(viewer.getViewerFrame().getByText('Hello World')).toBeVisible({ timeout: 30000 });
+ });
- this.timeout(90000);
+ // FastAPI is not working as expected on Ubuntu
+ test('Python - Verify Basic FastAPI App [C903306]', async function ({ app, python }) {
+ const viewer = app.workbench.positronViewer;
- const app = this.app as Application;
- const viewer = app.workbench.positronViewer;
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'fastapi_example', 'fastapi_example.py'));
+ await app.workbench.positronEditor.pressPlay();
+ await expect(viewer.getViewerFrame().getByText('FastAPI')).toBeVisible({ timeout: 30000 });
+ });
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'gradio_example', 'gradio_example.py'));
- await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronEditor.pressPlay();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await expect(async () => {
- await expect(viewer.getViewerFrame().getByRole('button', { name: 'Submit' })).toBeVisible({ timeout: 30000 });
- }).toPass({ timeout: 60000 });
- await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
- });
+ test('Python - Verify Basic Gradio App [C903307]', { tag: ['@win'] }, async function ({ app, python }) {
+ const viewer = app.workbench.positronViewer;
- it('Python - Verify Basic Streamlit App [C903308] #web #win', async function () {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'gradio_example', 'gradio_example.py'));
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronEditor.pressPlay();
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await expect(viewer.getViewerFrame().getByRole('button', { name: 'Submit' })).toBeVisible({ timeout: 45000 });
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
+ });
- this.timeout(90000);
+ test('Python - Verify Basic Streamlit App [C903308]', { tag: ['@web', '@win'] }, async function ({ app, python }) {
+ const viewer = app.workbench.positronViewer;
- const app = this.app as Application;
- const viewer = app.workbench.positronViewer;
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'streamlit_example', 'streamlit_example.py'));
+ await app.workbench.positronEditor.pressPlay();
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'streamlit_example', 'streamlit_example.py'));
- await app.workbench.positronEditor.pressPlay();
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ const viewerFrame = viewer.getViewerFrame();
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
- const viewerFrame = viewer.getViewerFrame();
- const headerLocator = this.app.web
+ await expect(async () => {
+ const headerLocator = app.web
? viewerFrame.frameLocator('iframe').getByRole('button', { name: 'Deploy' })
: viewerFrame.getByRole('button', { name: 'Deploy' });
- await expect(async () => {
- await expect(headerLocator).toBeVisible({ timeout: 30000 });
- }).toPass({ timeout: 60000 });
-
- await app.workbench.positronLayouts.enterLayout('stacked');
- });
-
- it('Python - Verify Basic Flask App [C1013655] #win #web', async function () {
- this.timeout(90000);
-
- const app = this.app as Application;
- const viewer = app.workbench.positronViewer;
-
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'flask_example', '__init__.py'));
- await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronEditor.pressPlay();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- const viewerFrame = viewer.getViewerFrame();
- const loginLocator = this.app.web
- ? viewerFrame.frameLocator('iframe').getByText('Log In')
- : viewerFrame.getByText('Log In');
-
- await expect(async () => {
- await expect(loginLocator).toBeVisible({ timeout: 30000 });
- }).toPass({ timeout: 60000 });
- await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
- });
+ await expect(headerLocator).toBeVisible({ timeout: 30000 });
+ }).toPass({ timeout: 60000 });
+ });
+ test('Python - Verify Basic Flask App [C1013655]', {
+ tag: ['@web', '@win']
+ }, async function ({ app, python, page }) {
+ const viewer = app.workbench.positronViewer;
+
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'python_apps', 'flask_example', '__init__.py'));
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronEditor.pressPlay();
+ await expect(page.locator('.notifications-toasts')).not.toBeVisible({ timeout: 30000 });
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ const viewerFrame = viewer.getViewerFrame();
+ const loginLocator = app.web
+ ? viewerFrame.frameLocator('iframe').getByText('Log In')
+ : viewerFrame.getByText('Log In');
+
+ await expect(async () => {
+ await expect(loginLocator).toBeVisible({ timeout: 30000 });
+ }).toPass({ timeout: 60000 });
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
});
});
diff --git a/test/smoke/src/areas/positron/apps/shiny.test.ts b/test/smoke/src/areas/positron/apps/shiny.test.ts
index 3bcd11e0f25..c00d5435238 100644
--- a/test/smoke/src/areas/positron/apps/shiny.test.ts
+++ b/test/smoke/src/areas/positron/apps/shiny.test.ts
@@ -3,77 +3,44 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-import { expect } from '@playwright/test';
import { join } from 'path';
+import { test, expect } from '../_test.setup';
-describe('Shiny Application #win', () => {
- setupAndStartApp();
+test.use({
+ suiteId: __filename
+});
- before(async function () {
+test.describe('Shiny Application', {
+}, () => {
+ test.beforeAll(async function ({ app }) {
try {
- await this.app.workbench.extensions.installExtension('posit.shiny', true);
- await this.app.workbench.extensions.closeExtension('Shiny');
+ await app.workbench.extensions.installExtension('posit.shiny', true);
+ await app.workbench.extensions.closeExtension('Shiny');
} catch (e) {
- this.app.code.driver.takeScreenshot('shinySetup');
+ app.code.driver.takeScreenshot('shinySetup');
throw e;
}
});
- describe('Shiny Application - Python', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- it('Python - Verify Basic Shiny App [C699099]', async function () {
- const app = this.app as Application;
-
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'shiny-py-example', 'app.py'));
-
- await app.workbench.quickaccess.runCommand('shiny.python.runApp');
-
- const headerLocator = app.workbench.positronViewer.getViewerLocator('h1');
-
- await expect(headerLocator).toHaveText('Restaurant tipping', { timeout: 20000 });
-
- await app.workbench.positronTerminal.sendKeysToTerminal('Control+C');
-
- await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('Application shutdown complete.')));
-
- // refresh the viewer so the shutdown Python app goes away before we kick off the R app
- await app.workbench.positronViewer.refreshViewer();
- });
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronTerminal.sendKeysToTerminal('Control+C');
+ await app.workbench.positronViewer.refreshViewer();
});
- describe('Shiny Application - R', () => {
- before(async function () {
- // setup R but do not wait for a default interpreter to finish starting
- await PositronRFixtures.SetupFixtures(this.app as Application, true);
- });
-
- it('R - Verify Basic Shiny App [C699100]', async function () {
- const app = this.app as Application;
+ test('Python - Verify Basic Shiny App [C699099]', async function ({ app, python }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'shiny-py-example', 'app.py'));
+ await app.workbench.quickaccess.runCommand('shiny.python.runApp');
+ const headerLocator = app.workbench.positronViewer.getViewerLocator('h1');
+ await expect(headerLocator).toHaveText('Restaurant tipping', { timeout: 20000 });
+ });
- const code = `library(shiny)
+ test('R - Verify Basic Shiny App [C699100]', async function ({ app, r }) {
+ const code = `library(shiny)
runExample("01_hello")`;
-
- await app.workbench.positronConsole.pasteCodeToConsole(code);
-
- await app.workbench.positronConsole.sendEnterKey();
-
- const headerLocator = app.workbench.positronViewer.getViewerLocator('h1');
-
- await expect(headerLocator).toHaveText('Hello Shiny!', { timeout: 20000 });
-
- await app.workbench.positronConsole.activeConsole.click();
- await app.workbench.positronConsole.sendKeyboardKey('Control+C');
-
- // not strictly needed yet, but in case another case is added later afterwards
- // make sure that the shut down R app is not present
- await app.workbench.positronViewer.refreshViewer();
-
- });
+ await app.workbench.positronConsole.pasteCodeToConsole(code);
+ await app.workbench.positronConsole.sendEnterKey();
+ const headerLocator = app.workbench.positronViewer.getViewerLocator('h1');
+ await expect(headerLocator).toHaveText('Hello Shiny!', { timeout: 20000 });
});
});
diff --git a/test/smoke/src/areas/positron/connections/connections-db.test.ts b/test/smoke/src/areas/positron/connections/connections-db.test.ts
new file mode 100644
index 00000000000..ed7475583f8
--- /dev/null
+++ b/test/smoke/src/areas/positron/connections/connections-db.test.ts
@@ -0,0 +1,100 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('SQLite DB Connection', { tag: ['@web', '@win'] }, () => {
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronConnections.removeConnectionButton.click();
+ });
+
+ test('Python - SQLite DB Connection [C628636]', async function ({ app, logger, python }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'chinook-db-py', 'chinook-sqlite.py'));
+ await app.workbench.quickaccess.runCommand('python.execInConsole');
+
+ await expect(async () => {
+ logger.log('Opening connections pane');
+ await app.workbench.positronVariables.doubleClickVariableRow('conn');
+ // in Python this will open all table connections, so should be fine.
+ await app.workbench.positronConnections.openTree();
+
+ // click in reverse order to avoid scrolling issues
+ await app.workbench.positronConnections.hasConnectionNodes(['albums']);
+ }).toPass({ timeout: 60000 });
+
+ // disconnect icon appearance requires hover
+ await app.workbench.positronConnections.pythonConnectionOpenState.hover();
+ await app.workbench.positronConnections.disconnectButton.click();
+ await app.workbench.positronConnections.reconnectButton.waitforVisible();
+ });
+
+
+ test('R - SQLite DB Connection [C628637]', async function ({ app, logger, r }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'chinook-db-r', 'chinook-sqlite.r'));
+ await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
+
+ await expect(async () => {
+ logger.log('Opening connections pane');
+ await app.workbench.positronConnections.openConnectionPane();
+ await app.workbench.positronConnections.openTree();
+
+ // click in reverse order to avoid scrolling issues
+ // in R, the opneTree command only shows all tables, we click to also
+ // display fields
+ await app.workbench.positronConnections.openConnectionsNodes(tables);
+ }).toPass({ timeout: 60000 });
+
+ // disconnect icon appearance requires hover
+ await app.workbench.positronConnections.rConnectionOpenState.hover();
+ await app.workbench.positronConnections.disconnectButton.click();
+ await app.workbench.positronConnections.reconnectButton.waitforVisible();
+ });
+
+ test('R - Connections are update after adding a database,[C663724]', async function ({ app, logger, r }) {
+ // open an empty connection
+ await app.workbench.positronConsole.executeCode(
+ 'R',
+ `con <- connections::connection_open(RSQLite::SQLite(), tempfile())`,
+ '>'
+ );
+
+ // should be able to see the new connection in the connections pane
+ logger.log('Opening connections pane');
+ await app.workbench.positronConnections.connectionsTabLink.click();
+
+ await app.workbench.positronConnections.openTree();
+
+ const visible = await app.workbench.positronConnections.hasConnectionNode("mtcars");
+ if (visible) {
+ throw new Error("mtcars should not be visible");
+ }
+
+ await expect(async () => {
+ // now we add a dataframe to that connection
+ await app.workbench.positronConsole.executeCode(
+ 'R',
+ `DBI::dbWriteTable(con, "mtcars", mtcars)`,
+ '>'
+ );
+ // the panel should be automatically updated and we should be able to see
+ // that table and click on it
+ await app.workbench.positronConnections.openConnectionsNodes(["mtcars"]);
+ }).toPass();
+
+ // disconnect icon appearance requires hover
+ await app.workbench.positronConnections.rConnectionOpenState.hover();
+ await app.workbench.positronConnections.disconnectButton.click();
+ await app.workbench.positronConnections.reconnectButton.waitforVisible();
+ });
+
+});
+
+
+const tables = ['tracks', 'playlist_track', 'playlists', 'media_types', 'invoice_items', 'invoices', 'genres', 'employees', 'customers', 'artists', 'albums'];
diff --git a/test/smoke/src/areas/positron/connections/dbConnections.test.ts b/test/smoke/src/areas/positron/connections/dbConnections.test.ts
deleted file mode 100644
index 61a7597c3d0..00000000000
--- a/test/smoke/src/areas/positron/connections/dbConnections.test.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { join } from 'path';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { expect } from '@playwright/test';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-let logger;
-const tables = ['tracks', 'playlist_track', 'playlists', 'media_types', 'invoice_items', 'invoices', 'genres', 'employees', 'customers', 'artists', 'albums'];
-
-describe('Connections Pane #web #win', () => {
- logger = setupAndStartApp();
-
- describe('Python - SQLite DB', () => {
-
- before(async function () {
-
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
- });
-
- after(async function () {
-
- const app = this.app as Application;
- await app.workbench.positronConnections.removeConnectionButton.click();
-
- });
-
- it('Python - SQLite DB Connection [C628636]', async function () {
-
- const app = this.app as Application;
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'chinook-db-py', 'chinook-sqlite.py'));
- await app.workbench.quickaccess.runCommand('python.execInConsole');
-
- await expect(async () => {
- logger.log('Opening connections pane');
- await app.workbench.positronVariables.doubleClickVariableRow('conn');
- // in Python this will open all table connections, so should be fine.
- await app.workbench.positronConnections.openTree();
-
- // click in reverse order to avoid scrolling issues
- await app.workbench.positronConnections.hasConnectionNodes(['albums']);
- }).toPass({ timeout: 60000 });
-
- // disconnect icon appearance requires hover
- await app.workbench.positronConnections.pythonConnectionOpenState.hover();
- await app.workbench.positronConnections.disconnectButton.click();
- await app.workbench.positronConnections.reconnectButton.waitforVisible();
- });
- });
-
- describe('R - SQLite DB', () => {
-
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- });
-
- afterEach(async function () {
-
- const app = this.app as Application;
- await app.workbench.positronConnections.removeConnectionButton.click();
-
- });
-
- it('R - SQLite DB Connection [C628637]', async function () {
-
- const app = this.app as Application;
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'chinook-db-r', 'chinook-sqlite.r'));
- await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
-
- await expect(async () => {
- logger.log('Opening connections pane');
- await app.workbench.positronConnections.openConnectionPane();
- await app.workbench.positronConnections.openTree();
-
- // click in reverse order to avoid scrolling issues
- // in R, the opneTree command only shows all tables, we click to also
- // display fields
- await app.workbench.positronConnections.openConnectionsNodes(tables);
- }).toPass({ timeout: 60000 });
-
- // disconnect icon appearance requires hover
- await app.workbench.positronConnections.rConnectionOpenState.hover();
- await app.workbench.positronConnections.disconnectButton.click();
- await app.workbench.positronConnections.reconnectButton.waitforVisible();
- });
-
- it('R - Connections are update after adding a database,[C663724]', async function () {
-
- const app = this.app as Application;
-
- // open an empty connection
- await app.workbench.positronConsole.executeCode(
- 'R',
- `con <- connections::connection_open(RSQLite::SQLite(), tempfile())`,
- '>'
- );
-
- // should be able to see the new connection in the connections pane
- logger.log('Opening connections pane');
- await app.workbench.positronConnections.connectionsTabLink.click();
-
- await app.workbench.positronConnections.openTree();
-
- const visible = await app.workbench.positronConnections.hasConnectionNode("mtcars");
- if (visible) {
- throw new Error("mtcars should not be visible");
- }
-
- await expect(async () => {
- // now we add a dataframe to that connection
- await app.workbench.positronConsole.executeCode(
- 'R',
- `DBI::dbWriteTable(con, "mtcars", mtcars)`,
- '>'
- );
- // the panel should be automatically updated and we should be able to see
- // that table and click on it
- await app.workbench.positronConnections.openConnectionsNodes(["mtcars"]);
- }).toPass();
-
- // disconnect icon appearance requires hover
- await app.workbench.positronConnections.rConnectionOpenState.hover();
- await app.workbench.positronConnections.disconnectButton.click();
- await app.workbench.positronConnections.reconnectButton.waitforVisible();
-
- });
-
- });
-});
-
diff --git a/test/smoke/src/areas/positron/console/console-ansi.test.ts b/test/smoke/src/areas/positron/console/console-ansi.test.ts
new file mode 100644
index 00000000000..6645ae304b9
--- /dev/null
+++ b/test/smoke/src/areas/positron/console/console-ansi.test.ts
@@ -0,0 +1,78 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Console ANSI styling', { tag: ['@pr'] }, () => {
+ test.beforeEach(async function ({ app }) {
+ await app.workbench.positronLayouts.enterLayout('fullSizedPanel');
+ });
+
+ test("R - Can produce clickable file links [C683069]", async function ({ app, r }) {
+ // Can be any file on the workkspace. We use .gitignore as it's probably
+ // always there.
+ const fileName = '.gitignore';
+ const filePath = join(app.workspacePathOrFolder, fileName);
+ const inputCode = `cli::cli_inform(r"[{.file ${filePath}}]")`;
+
+ await expect(async () => {
+ await app.workbench.positronConsole.pasteCodeToConsole(inputCode);
+ await app.workbench.positronConsole.sendEnterKey();
+
+ // Locate the link and click on it
+ const link = app.workbench.positronConsole.getLastClickableLink();
+ await expect(link).toContainText(fileName, { useInnerText: true });
+
+ await link.click();
+ await app.workbench.editors.waitForActiveTab(fileName);
+ }).toPass({ timeout: 60000 });
+ });
+
+ test("R - Can produce clickable help links [C683070]", async function ({ app, r }) {
+ const inputCode = `cli::cli_inform("{.fun base::mean}")`;
+
+ await expect(async () => {
+ await app.workbench.positronConsole.pasteCodeToConsole(inputCode);
+ await app.workbench.positronConsole.sendEnterKey();
+
+ // Locate the link and click on it
+ const link = app.workbench.positronConsole.getLastClickableLink();
+ await expect(link).toContainText('base::mean', { useInnerText: true });
+
+ await link.click();
+ await app.code.wait(200);
+
+ const helpFrame = await app.workbench.positronHelp.getHelpFrame(0);
+ await expect(helpFrame.locator('body')).toContainText('Arithmetic Mean');
+ }).toPass({ timeout: 60000 });
+ });
+
+ test("R - Can produce colored output [C683071]", async function ({ app, r }) {
+ const color = '#ff3333';
+ const rgb_color = "rgb(255, 51, 51)"; // same as above but in rgb
+
+ await expect(async () => {
+ await app.workbench.positronConsole.pasteCodeToConsole(
+ `
+ cli::cli_div(theme = list(span.emph = list(color = "${color}")))
+ cli::cli_text("This is very {.emph important}")
+ cli::cli_end()
+ `
+ );
+ }).toPass();
+
+ await app.workbench.positronConsole.sendEnterKey();
+
+ const styled_locator = app.workbench.positronConsole.activeConsole.getByText("important").last();
+ await expect(styled_locator).toHaveCSS('font-style', 'italic');
+ await expect(styled_locator).toHaveCSS('color', rgb_color);
+ });
+});
+
diff --git a/test/smoke/src/areas/positron/console/console-autocomplete.test.ts b/test/smoke/src/areas/positron/console/console-autocomplete.test.ts
new file mode 100644
index 00000000000..8177fb1effb
--- /dev/null
+++ b/test/smoke/src/areas/positron/console/console-autocomplete.test.ts
@@ -0,0 +1,39 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test } from '../_test.setup';
+import { fail } from 'assert';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Console Autocomplete', {
+ tag: ['@web', '@win']
+}, () => {
+ test('Python - Verify Console Autocomplete [C947968]', async function ({ app, python }) {
+ await app.workbench.positronConsole.pasteCodeToConsole('import pandas as pd');
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronConsole.typeToConsole('df = pd.Dat');
+
+ const suggestionList = await app.workbench.positronConsole.getSuggestions();
+ if (suggestionList.length < 3) {
+ fail('Less than 3 suggestions found');
+ }
+ });
+
+ test('R - Verify Console Autocomplete [C947969]', async function ({ app, r }) {
+ await app.workbench.positronConsole.pasteCodeToConsole('library(arrow)');
+ await app.workbench.positronConsole.sendEnterKey();
+
+ // need to type to console slowly to see suggestions with R
+ await app.workbench.positronConsole.typeToConsole('df2 <- read_p', 250);
+
+ const suggestionList = await app.workbench.positronConsole.getSuggestions();
+ if (suggestionList.length < 3) {
+ fail('Less than 3 suggestions found');
+ }
+ });
+});
diff --git a/test/smoke/src/areas/positron/console/console-clipboard.test.ts b/test/smoke/src/areas/positron/console/console-clipboard.test.ts
new file mode 100644
index 00000000000..014c353935b
--- /dev/null
+++ b/test/smoke/src/areas/positron/console/console-clipboard.test.ts
@@ -0,0 +1,73 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as os from 'os';
+import { test, expect } from '../_test.setup';
+import { Application } from '../../../../../automation';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Console - Clipboard', () => {
+ test('Python - Copy from console & paste to console [C608100]', async function ({ app, python }) {
+ await testBody(app);
+ });
+
+ test('R - Copy from console & paste to console [C663725]', async function ({ app, r }) {
+ await testBody(app);
+ });
+});
+
+async function testBody(app: Application) {
+ const isMac = os.platform() === 'darwin';
+ const modifier = isMac ? 'Meta' : 'Control';
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+
+ const activeConsole = app.workbench.positronConsole.activeConsole;
+ await activeConsole.click();
+ const page = activeConsole!.page();
+
+ const testLine = 'a = 1';
+
+ await expect(async () => {
+ // Ensure nothing is in the current line and clear the console
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronConsole.barClearButton.click();
+
+ // Send test line to console
+ await app.workbench.positronConsole.typeToConsole(testLine);
+
+ // copy the test line and send enter key
+ await page.keyboard.press(`${modifier}+A`);
+ await page.keyboard.press(`${modifier}+C`);
+ await app.workbench.positronConsole.sendEnterKey();
+
+ // ensure the console previous lines contain the test line
+ await app.workbench.positronConsole.waitForConsoleContents(
+ (lines) => lines.some((line) => line.includes(testLine)));
+
+ // clear the console and ensure the clear succeeded
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.positronConsole.waitForConsoleContents((contents) => {
+ return !contents.some(Boolean);
+ });
+ }).toPass({ timeout: 40000 });
+
+ await page.keyboard.press(`${modifier}+V`);
+
+ await app.workbench.positronConsole.waitForCurrentConsoleLineContents((line) =>
+ line.includes(testLine.replaceAll(' ', 'Â ')));
+
+ await app.workbench.positronConsole.sendEnterKey();
+
+ // check for two instances of the test line in a row (invalid)
+ await app.workbench.positronConsole.waitForConsoleContents((contents) =>
+ contents.some((line) => line.includes(testLine))
+ );
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+}
diff --git a/test/smoke/src/areas/positron/console/console-history.test.ts b/test/smoke/src/areas/positron/console/console-history.test.ts
new file mode 100644
index 00000000000..94291ff19e0
--- /dev/null
+++ b/test/smoke/src/areas/positron/console/console-history.test.ts
@@ -0,0 +1,119 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Console History', {
+ tag: ['@web', '@win']
+}, () => {
+ test.afterEach(async function ({ app }) {
+ app.workbench.positronConsole.sendKeyboardKey('Escape');
+ });
+
+ test('Python - Verify Console History [C685945]', async function ({ app, python }) {
+ const lineOne = 'a = 1';
+ const lineTwo = 'b = 2';
+ const lineThree = 'c = 3';
+
+ await expect(async () => {
+ await app.workbench.positronConsole.typeToConsole(lineOne);
+ await app.workbench.positronConsole.sendEnterKey();
+
+ await app.workbench.positronConsole.waitForConsoleContents(
+ (lines) => lines.some((line) => line.includes(lineOne)));
+
+ await app.workbench.positronConsole.typeToConsole(lineTwo);
+ await app.workbench.positronConsole.sendEnterKey();
+
+ await app.workbench.positronConsole.waitForConsoleContents(
+ (lines) => lines.some((line) => line.includes(lineTwo)));
+
+ await app.workbench.positronConsole.typeToConsole(lineThree);
+ await app.workbench.positronConsole.sendEnterKey();
+
+ await app.workbench.positronConsole.waitForConsoleContents(
+ (lines) => lines.some((line) => line.includes(lineThree)));
+ }).toPass({ timeout: 40000 });
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
+ await app.workbench.positronConsole.barClearButton.click();
+
+ await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
+ await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
+ await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
+
+ await app.workbench.positronConsole.waitForCurrentConsoleLineContents((line) =>
+ line.includes('a = 1'));
+
+ await app.workbench.positronConsole.sendEnterKey();
+
+ await app.workbench.positronConsole.sendKeyboardKey('Control+R');
+
+ await app.workbench.positronConsole.waitForHistoryContents((contents) =>
+ contents.some((line) => line.includes(lineOne)) &&
+ contents.some((line) => line.includes(lineTwo)) &&
+ contents.some((line) => line.includes(lineThree)));
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+
+ });
+
+
+ test('R - Verify Console History [C685946]]', async function ({ app, r }) {
+ const lineOne = 'a <- 1';
+ const lineTwo = 'b <- 2';
+ const lineThree = 'c <- 3';
+ await expect(async () => {
+ // send test line one and the enter key, then expect it in the previous console
+ // lines
+ await app.workbench.positronConsole.typeToConsole(lineOne);
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronConsole.waitForConsoleContents(
+ (lines) => lines.some((line) => line.includes(lineOne)));
+
+ // send test line two and the enter key, then expect it in the previous console
+ // lines
+ await app.workbench.positronConsole.typeToConsole(lineTwo);
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronConsole.waitForConsoleContents(
+ (lines) => lines.some((line) => line.includes(lineTwo)));
+
+ // send test line three and the enter key, then expect it in the previous console
+ // lines
+ await app.workbench.positronConsole.typeToConsole(lineThree);
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronConsole.waitForConsoleContents(
+ (lines) => lines.some((line) => line.includes(lineThree)));
+
+ }).toPass({ timeout: 40000 });
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+
+ await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
+ await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
+ await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
+
+ await app.workbench.positronConsole.waitForCurrentConsoleLineContents((line) =>
+ line.includes('a <- 1'));
+
+ await app.workbench.positronConsole.sendEnterKey();
+
+ await app.workbench.positronConsole.sendKeyboardKey('Control+R');
+
+ await app.workbench.positronConsole.waitForHistoryContents((contents) =>
+ contents.some((line) => line.includes(lineOne)) &&
+ contents.some((line) => line.includes(lineTwo)) &&
+ contents.some((line) => line.includes(lineThree)));
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+
+ });
+});
diff --git a/test/smoke/src/areas/positron/console/consoleInput.test.ts b/test/smoke/src/areas/positron/console/console-input.test.ts
similarity index 71%
rename from test/smoke/src/areas/positron/console/consoleInput.test.ts
rename to test/smoke/src/areas/positron/console/console-input.test.ts
index 6b5a7c88261..e57e0584035 100644
--- a/test/smoke/src/areas/positron/console/consoleInput.test.ts
+++ b/test/smoke/src/areas/positron/console/console-input.test.ts
@@ -3,106 +3,78 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
+import { test, expect } from '../_test.setup';
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Console Input #web #pr #win', () => {
- setupAndStartApp();
+test.use({
+ suiteId: __filename
+});
- describe('Console Input - Python', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- await this.app.workbench.positronLayouts.enterLayout('fullSizedPanel');
- });
+test.describe('Console Input', {
+ tag: ['@web', '@pr', '@win']
+}, () => {
- after(async function () {
- const app = this.app as Application;
- await app.workbench.positronLayouts.enterLayout('stacked');
+ test.describe('Console Input - Python', () => {
+ test.beforeEach(async function ({ app, python }) {
+ await app.workbench.positronLayouts.enterLayout('fullSizedPanel');
});
- it('Python - Get Input String Console [C667516]', async function () {
- const app = this.app as Application;
-
+ test('Python - Get Input String Console [C667516]', async function ({ app }) {
const inputCode = `val = input("Enter your name: ")
print(f'Hello {val}!')`;
await expect(async () => {
await app.workbench.positronConsole.pasteCodeToConsole(inputCode);
-
await app.workbench.positronConsole.sendEnterKey();
-
await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Enter your name:')));
// slight wait before starting to type
await app.code.wait(200);
await app.workbench.positronConsole.typeToConsole('John Doe');
-
await app.workbench.positronConsole.sendEnterKey();
-
await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Hello John Doe!')));
}).toPass({ timeout: 60000 });
-
});
});
- describe('Console Input - R', () => {
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- await this.app.workbench.positronLayouts.enterLayout('fullSizedPanel');
- });
-
- after(async function () {
- const app = this.app as Application;
- await app.workbench.positronLayouts.enterLayout('stacked');
+ test.describe('Console Input - R', () => {
+ test.beforeEach(async function ({ app, r }) {
+ await app.workbench.positronLayouts.enterLayout('fullSizedPanel');
});
- it('R - Get Input String Console [C667517]', async function () {
- const app = this.app as Application;
-
+ test('R - Get Input String Console [C667517]', async function ({ app }) {
const inputCode = `val <- readline(prompt = "Enter your name: ")
cat(sprintf('Hello %s!\n', val))`;
await expect(async () => {
await app.workbench.positronConsole.pasteCodeToConsole(inputCode);
-
await app.workbench.positronConsole.sendEnterKey();
-
await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Enter your name:')));
// slight wait before starting to type
await app.code.wait(200);
-
await app.workbench.positronConsole.typeToConsole('John Doe');
-
await app.workbench.positronConsole.sendEnterKey();
-
await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Hello John Doe!')));
}).toPass({ timeout: 60000 });
});
- it('R - Can use `menu` to select alternatives [C684749]', async function () {
- const app = this.app as Application;
+ test('R - Can use `menu` to select alternatives [C684749]', async function ({ app }) {
const inputCode = `x <- menu(letters)`;
await expect(async () => {
await app.workbench.positronConsole.pasteCodeToConsole(inputCode);
await app.workbench.positronConsole.sendEnterKey();
-
await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Selection:')));
// slight wait before starting to type
await app.code.wait(200);
-
await app.workbench.positronConsole.typeToConsole('1');
await app.workbench.positronConsole.sendEnterKey();
// slight wait before starting to type
await app.code.wait(200);
-
await app.workbench.positronConsole.typeToConsole('x');
await app.workbench.positronConsole.sendEnterKey();
@@ -110,15 +82,14 @@ cat(sprintf('Hello %s!\n', val))`;
}).toPass({ timeout: 60000 });
});
- it("R - Esc only dismisses autocomplete not full text typed into console [C685868]", async function () {
+ test("R - Esc only dismisses autocomplete not full text typed into console [C685868]", async function ({ app }) {
// This is a regression test for https://github.com/posit-dev/positron/issues/1161
- const app = this.app as Application;
const inputCode = `base::mea`;
await expect(async () => {
await app.workbench.positronConsole.typeToConsole(inputCode);
- }).toPass({ timeout: 600 });
+ }).toPass({ timeout: 60000 });
const activeConsole = app.workbench.positronConsole.activeConsole;
diff --git a/test/smoke/src/areas/positron/console/console-output.test.ts b/test/smoke/src/areas/positron/console/console-output.test.ts
new file mode 100644
index 00000000000..9fef9ed6c4e
--- /dev/null
+++ b/test/smoke/src/areas/positron/console/console-output.test.ts
@@ -0,0 +1,40 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Console Output', { tag: ['@win'] }, () => {
+ test('R - Console output in a loop with short pauses [C885225]', async function ({ app, r }) {
+ await app.workbench.positronConsole.pasteCodeToConsole(rCode);
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Why do programmers prefer dark mode')));
+ await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Because light attracts bugs!')));
+ });
+});
+
+const rCode = `tokens <- c(
+ "",
+ "Why",
+ " do",
+ " programmers",
+ " prefer",
+ " dark",
+ " mode",
+ "?\n\n",
+ "Because",
+ " light",
+ " attracts",
+ " bugs",
+ "!"
+ )
+
+ for(token in tokens) {
+ cat(token)
+ Sys.sleep(0.01)
+ }`;
diff --git a/test/smoke/src/areas/positron/console/console-python.test.ts b/test/smoke/src/areas/positron/console/console-python.test.ts
new file mode 100644
index 00000000000..92edbaadede
--- /dev/null
+++ b/test/smoke/src/areas/positron/console/console-python.test.ts
@@ -0,0 +1,45 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Console Pane: Python', { tag: ['@web', '@win'] }, () => {
+
+ test('Verify restart button inside the console [C377918]', async function ({ app, python }) {
+ await expect(async () => {
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+
+ // workaround issue where power button click fails
+ await app.code.wait(1000);
+ await app.workbench.positronConsole.barPowerButton.click();
+ await app.workbench.positronConsole.consoleRestartButton.click();
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.waitForReady('>>>');
+ await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
+ await app.workbench.positronConsole.consoleRestartButton.isNotVisible();
+ }).toPass();
+ });
+
+ test('Verify restart button on console bar [C617464]', {
+ }, async function ({ app, python }) {
+ // Need to make console bigger to see all bar buttons
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+
+ // workaround issue where "started" text never appears post restart
+ await app.code.wait(1000);
+ await app.workbench.positronConsole.barRestartButton.click();
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.waitForReady('>>>');
+ await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
+ });
+});
diff --git a/test/smoke/src/areas/positron/console/console-r.test.ts b/test/smoke/src/areas/positron/console/console-r.test.ts
new file mode 100644
index 00000000000..8633334cf8d
--- /dev/null
+++ b/test/smoke/src/areas/positron/console/console-r.test.ts
@@ -0,0 +1,40 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Console Pane: R', {
+ tag: ['@web', '@win']
+}, () => {
+ test.beforeAll(async function ({ app }) {
+ // Need to make console bigger to see all bar buttons
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ });
+
+ test('Verify restart button inside the console [C377917]', async function ({ app, r }) {
+ await expect(async () => {
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.positronConsole.barPowerButton.click();
+ await app.workbench.positronConsole.consoleRestartButton.click();
+ await app.workbench.positronConsole.waitForReady('>');
+ await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
+ await app.workbench.positronConsole.consoleRestartButton.isNotVisible();
+ }).toPass();
+ });
+
+ test('Verify restart button on console bar [C620636]', async function ({ app, r }) {
+ await expect(async () => {
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.positronConsole.barRestartButton.click();
+ await app.workbench.positronConsole.waitForReady('>');
+ await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
+ }).toPass();
+ });
+});
+
diff --git a/test/smoke/src/areas/positron/console/consoleANSI.test.ts b/test/smoke/src/areas/positron/console/consoleANSI.test.ts
deleted file mode 100644
index 371f8826d4c..00000000000
--- a/test/smoke/src/areas/positron/console/consoleANSI.test.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { expect } from '@playwright/test';
-import { Application, PositronRFixtures } from '../../../../../automation';
-import { join } from 'path';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Console ANSI styling', () => {
- setupAndStartApp();
-
- describe('R - Console ANSI styling', () => {
-
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- await this.app.workbench.positronLayouts.enterLayout('fullSizedPanel');
- });
-
- after(async function () {
- const app = this.app as Application;
- await app.workbench.positronLayouts.enterLayout('stacked');
- });
-
- it("R - Can produce clickable file links [C683069] #pr", async function () {
- const app = this.app as Application;
-
- // Can be any file on the workkspace. We use .gitignore as it's probably
- // always there.
- const fileName = '.gitignore';
- const filePath = join(app.workspacePathOrFolder, fileName);
- const inputCode = `cli::cli_inform(r"[{.file ${filePath}}]")`;
-
- await expect(async () => {
- await app.workbench.positronConsole.pasteCodeToConsole(inputCode);
- await app.workbench.positronConsole.sendEnterKey();
-
- // Locate the link and click on it
- const link = app.workbench.positronConsole.getLastClickableLink();
- await expect(link).toContainText(fileName, { useInnerText: true });
-
- await link.click();
- await app.workbench.editors.waitForActiveTab(fileName);
- }).toPass({ timeout: 60000 });
- });
-
- it("R - Can produce clickable help links [C683070] #pr", async function () {
- const app = this.app as Application;
- const inputCode = `cli::cli_inform("{.fun base::mean}")`;
-
- await expect(async () => {
- await app.workbench.positronConsole.pasteCodeToConsole(inputCode);
- await app.workbench.positronConsole.sendEnterKey();
-
- // Locate the link and click on it
- const link = app.workbench.positronConsole.getLastClickableLink();
- await expect(link).toContainText('base::mean', { useInnerText: true });
-
- await link.click();
- await app.code.wait(200);
-
- const helpFrame = await app.workbench.positronHelp.getHelpFrame(0);
- await expect(helpFrame.locator('body')).toContainText('Arithmetic Mean');
- }).toPass({ timeout: 60000 });
- });
-
- it("R - Can produce colored output [C683071] #pr", async function () {
- const app = this.app as Application;
-
- const color = '#ff3333';
- const rgb_color = "rgb(255, 51, 51)"; // same as above but in rgb
-
- await expect(async () => {
- await app.workbench.positronConsole.pasteCodeToConsole(
- `
- cli::cli_div(theme = list(span.emph = list(color = "${color}")))
- cli::cli_text("This is very {.emph important}")
- cli::cli_end()
- `
- );
- }).toPass();
-
- await app.workbench.positronConsole.sendEnterKey();
-
- const styled_locator = app.workbench.positronConsole.activeConsole.getByText("important").last();
- await expect(styled_locator).toHaveCSS('font-style', 'italic');
- await expect(styled_locator).toHaveCSS('color', rgb_color);
- });
- });
-});
-
diff --git a/test/smoke/src/areas/positron/console/consoleAutocomplete.test.ts b/test/smoke/src/areas/positron/console/consoleAutocomplete.test.ts
deleted file mode 100644
index dec5b54e96c..00000000000
--- a/test/smoke/src/areas/positron/console/consoleAutocomplete.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-import { fail } from 'assert';
-
-
-describe('Console Autocomplete #web #win', () => {
- setupAndStartApp();
-
- describe('Console Autocomplete - Python', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- it('Python - Verify Console Autocomplete [C947968]', async function () {
- const app = this.app as Application;
-
- await app.workbench.positronConsole.pasteCodeToConsole('import pandas as pd');
- await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronConsole.typeToConsole('df = pd.Dat');
-
- const suggestionList = await app.workbench.positronConsole.getSuggestions();
-
- if (suggestionList.length < 3) {
- fail('Less than 3 suggestions found');
- }
- });
- });
-
-
- describe('Console Autocomplete - R', () => {
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- it('R - Verify Console Autocomplete [C947969]', async function () {
- const app = this.app as Application;
-
- await app.workbench.positronConsole.pasteCodeToConsole('library(arrow)');
- await app.workbench.positronConsole.sendEnterKey();
-
- // need to type to console slowly to see suggestions with R
- await app.workbench.positronConsole.typeToConsole('df2 <- read_p', 250);
-
- const suggestionList = await app.workbench.positronConsole.getSuggestions();
-
- if (suggestionList.length < 3) {
- fail('Less than 3 suggestions found');
- }
- });
- });
-
-});
diff --git a/test/smoke/src/areas/positron/console/consoleClipboard.test.ts b/test/smoke/src/areas/positron/console/consoleClipboard.test.ts
deleted file mode 100644
index 851d6455b81..00000000000
--- a/test/smoke/src/areas/positron/console/consoleClipboard.test.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import * as os from 'os';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-
-describe('Console', () => {
- setupAndStartApp();
-
- const isMac = os.platform() === 'darwin';
- const modifier = isMac ? 'Meta' : 'Control';
-
- async function testBody(app: Application) {
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
-
- const activeConsole = app.workbench.positronConsole.activeConsole;
- await activeConsole.click();
- const page = activeConsole!.page();
-
- const testLine = 'a = 1';
-
- await expect(async () => {
- // Ensure nothing is in the current line and clear the console
- await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronConsole.barClearButton.click();
-
- // Send test line to console
- await app.workbench.positronConsole.typeToConsole(testLine);
-
- // copy the test line and send enter key
- await page.keyboard.press(`${modifier}+A`);
- await page.keyboard.press(`${modifier}+C`);
- await app.workbench.positronConsole.sendEnterKey();
-
- // ensure the console previous lines contain the test line
- await app.workbench.positronConsole.waitForConsoleContents(
- (lines) => lines.some((line) => line.includes(testLine)));
-
- // clear the console and ensure the clear succeeded
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.positronConsole.waitForConsoleContents((contents) => {
- return !contents.some(Boolean);
- });
- }).toPass({ timeout: 40000 });
-
- await page.keyboard.press(`${modifier}+V`);
-
- await app.workbench.positronConsole.waitForCurrentConsoleLineContents((line) =>
- line.includes(testLine.replaceAll(' ', 'Â ')));
-
- await app.workbench.positronConsole.sendEnterKey();
-
- // check for two instances of the test line in a row (invalid)
- await app.workbench.positronConsole.waitForConsoleContents((contents) =>
- contents.some((line) => line.includes(testLine))
- );
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- }
-
- describe('Console Clipboard - Python', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- it('Python - Copy from console & paste to console [C608100]', async function () {
- await testBody(this.app);
- });
- });
-
- describe('Console Clipboard - R', () => {
- before(async function () {
- // setup R but do not wait for a default interpreter to finish starting
- await PositronRFixtures.SetupFixtures(this.app as Application, true);
- });
-
- it('R - Copy from console & paste to console [C663725]', async function () {
- await testBody(this.app);
- });
- });
-});
diff --git a/test/smoke/src/areas/positron/console/consoleHistory.test.ts b/test/smoke/src/areas/positron/console/consoleHistory.test.ts
deleted file mode 100644
index 461a3b46cee..00000000000
--- a/test/smoke/src/areas/positron/console/consoleHistory.test.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-
-describe('Console History #web #win', () => {
- setupAndStartApp();
-
- describe('Console History - Python', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- after(async function () {
- this.app.workbench.positronConsole.sendKeyboardKey('Escape');
- });
-
- const lineOne = 'a = 1';
- const lineTwo = 'b = 2';
- const lineThree = 'c = 3';
- it('Python - Verify Console History [C685945]', async function () {
- const app = this.app as Application;
-
- await expect(async () => {
- await app.workbench.positronConsole.typeToConsole(lineOne);
- await app.workbench.positronConsole.sendEnterKey();
-
- await app.workbench.positronConsole.waitForConsoleContents(
- (lines) => lines.some((line) => line.includes(lineOne)));
-
- await app.workbench.positronConsole.typeToConsole(lineTwo);
- await app.workbench.positronConsole.sendEnterKey();
-
- await app.workbench.positronConsole.waitForConsoleContents(
- (lines) => lines.some((line) => line.includes(lineTwo)));
-
- await app.workbench.positronConsole.typeToConsole(lineThree);
- await app.workbench.positronConsole.sendEnterKey();
-
- await app.workbench.positronConsole.waitForConsoleContents(
- (lines) => lines.some((line) => line.includes(lineThree)));
- }).toPass({ timeout: 40000 });
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
- await app.workbench.positronConsole.barClearButton.click();
-
- await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
- await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
- await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
-
- await app.workbench.positronConsole.waitForCurrentConsoleLineContents((line) =>
- line.includes('a = 1'));
-
- await app.workbench.positronConsole.sendEnterKey();
-
- await app.workbench.positronConsole.sendKeyboardKey('Control+R');
-
- await app.workbench.positronConsole.waitForHistoryContents((contents) =>
- contents.some((line) => line.includes(lineOne)) &&
- contents.some((line) => line.includes(lineTwo)) &&
- contents.some((line) => line.includes(lineThree)));
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
-
- });
- });
-
- describe('Console History - R', () => {
- before(async function () {
- // setup R but do not wait for a default interpreter to finish starting
- await PositronRFixtures.SetupFixtures(this.app as Application, true);
- });
-
- after(async function () {
- this.app.workbench.positronConsole.sendKeyboardKey('Escape');
- });
-
- it('R - Verify Console History [C685946]]', async function () {
- const app = this.app as Application;
-
- const lineOne = 'a <- 1';
- const lineTwo = 'b <- 2';
- const lineThree = 'c <- 3';
- await expect(async () => {
- // send test line one and the enter key, then expect it in the previous console
- // lines
- await app.workbench.positronConsole.typeToConsole(lineOne);
- await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronConsole.waitForConsoleContents(
- (lines) => lines.some((line) => line.includes(lineOne)));
-
- // send test line two and the enter key, then expect it in the previous console
- // lines
- await app.workbench.positronConsole.typeToConsole(lineTwo);
- await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronConsole.waitForConsoleContents(
- (lines) => lines.some((line) => line.includes(lineTwo)));
-
- // send test line three and the enter key, then expect it in the previous console
- // lines
- await app.workbench.positronConsole.typeToConsole(lineThree);
- await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronConsole.waitForConsoleContents(
- (lines) => lines.some((line) => line.includes(lineThree)));
-
- }).toPass({ timeout: 40000 });
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
-
- await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
- await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
- await app.workbench.positronConsole.sendKeyboardKey('ArrowUp');
-
- await app.workbench.positronConsole.waitForCurrentConsoleLineContents((line) =>
- line.includes('a <- 1'));
-
- await app.workbench.positronConsole.sendEnterKey();
-
- await app.workbench.positronConsole.sendKeyboardKey('Control+R');
-
- await app.workbench.positronConsole.waitForHistoryContents((contents) =>
- contents.some((line) => line.includes(lineOne)) &&
- contents.some((line) => line.includes(lineTwo)) &&
- contents.some((line) => line.includes(lineThree)));
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
-
- });
- });
-});
diff --git a/test/smoke/src/areas/positron/console/consoleOutput.test.ts b/test/smoke/src/areas/positron/console/consoleOutput.test.ts
deleted file mode 100644
index af4293b574b..00000000000
--- a/test/smoke/src/areas/positron/console/consoleOutput.test.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { Application, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Console Output #win', () => {
- setupAndStartApp();
-
- describe('Console Output - R', () => {
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- it('R - Console output in a loop with short pauses [C885225]', async function () {
- const app = this.app as Application;
-
- const code = `tokens <- c(
-"",
-"Why",
-" do",
-" programmers",
-" prefer",
-" dark",
-" mode",
-"?\n\n",
-"Because",
-" light",
-" attracts",
-" bugs",
-"!"
-)
-
-for(token in tokens) {
- cat(token)
- Sys.sleep(0.01)
-}`;
-
- await app.workbench.positronConsole.pasteCodeToConsole(code);
-
- await app.workbench.positronConsole.sendEnterKey();
-
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Why do programmers prefer dark mode')));
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Because light attracts bugs!')));
-
- });
- });
-});
diff --git a/test/smoke/src/areas/positron/console/python-console.test.ts b/test/smoke/src/areas/positron/console/python-console.test.ts
deleted file mode 100644
index b3d7f7ad419..00000000000
--- a/test/smoke/src/areas/positron/console/python-console.test.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Console Pane: Python #web #win', () => {
- setupAndStartApp();
-
- describe('Python Console Restart', () => {
-
- beforeEach(async function () {
-
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
- });
-
- it('Verify restart button inside the console [C377918]', async function () {
-
- const app = this.app as Application;
- await expect(async () => {
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
-
- // workaround issue where power button click fails
- await app.code.wait(1000);
-
- await app.workbench.positronConsole.barPowerButton.click();
- await app.workbench.positronConsole.consoleRestartButton.click();
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
-
- await app.workbench.positronConsole.waitForReady('>>>');
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
- await app.workbench.positronConsole.consoleRestartButton.isNotVisible();
- }).toPass();
- });
-
- it('Verify restart button on console bar [C617464]', async function () {
- this.retries(1);
- const app = this.app as Application;
- // Need to make console bigger to see all bar buttons
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
-
- // workaround issue where "started" text never appears post restart
- await app.code.wait(1000);
-
- await app.workbench.positronConsole.barRestartButton.click();
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
-
- await app.workbench.positronConsole.waitForReady('>>>');
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
- });
-
- });
-
-});
diff --git a/test/smoke/src/areas/positron/console/r-console.test.ts b/test/smoke/src/areas/positron/console/r-console.test.ts
deleted file mode 100644
index 5e6a2c1b97b..00000000000
--- a/test/smoke/src/areas/positron/console/r-console.test.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { expect } from '@playwright/test';
-import { Application, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Console Pane: R', () => {
- setupAndStartApp();
-
- describe('R Console Restart #web #win', () => {
-
- before(async function () {
- // Need to make console bigger to see all bar buttons
- await this.app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- });
-
- beforeEach(async function () {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- });
-
- it('Verify restart button inside the console [C377917]', async function () {
- const app = this.app as Application;
- await expect(async () => {
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.positronConsole.barPowerButton.click();
- await app.workbench.positronConsole.consoleRestartButton.click();
- await app.workbench.positronConsole.waitForReady('>');
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
- await app.workbench.positronConsole.consoleRestartButton.isNotVisible();
- }).toPass();
- });
-
- it('Verify restart button on console bar [C620636]', async function () {
- const app = this.app as Application;
- await expect(async () => {
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.positronConsole.barRestartButton.click();
- await app.workbench.positronConsole.waitForReady('>');
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
- }).toPass();
- });
-
- });
-
-});
diff --git a/test/smoke/src/areas/positron/data-explorer/100x100-pandas.test.ts b/test/smoke/src/areas/positron/data-explorer/100x100-pandas.test.ts
new file mode 100644
index 00000000000..0ef80a156aa
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/100x100-pandas.test.ts
@@ -0,0 +1,29 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test } from '../_test.setup';
+import { join } from 'path';
+import { parquetFilePath, testDataExplorer } from './helpers/100x100';
+
+test.use({
+ suiteId: __filename
+});
+
+test('Data Explorer 100x100 - Python - Pandas [C557563]', { tag: ['@win'] }, async function ({ app, python }) {
+ test.slow();
+
+ const dataFrameName = 'pandas100x100';
+ await testDataExplorer(
+ app,
+ 'Python',
+ '>>>',
+ [
+ 'import pandas as pd',
+ `${dataFrameName} = pd.read_parquet("${parquetFilePath(app)}")`,
+ ],
+ dataFrameName,
+ join(app.workspacePathOrFolder, 'data-files', '100x100', 'pandas-100x100.tsv')
+ );
+});
diff --git a/test/smoke/src/areas/positron/data-explorer/100x100-polars.test.ts b/test/smoke/src/areas/positron/data-explorer/100x100-polars.test.ts
new file mode 100644
index 00000000000..e52117e2140
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/100x100-polars.test.ts
@@ -0,0 +1,30 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test } from '../_test.setup';
+import { join } from 'path';
+import { parquetFilePath, testDataExplorer } from './helpers/100x100';
+
+test.use({
+ suiteId: __filename
+});
+
+test('Data Explorer 100x100 - Python - Polars [C674520]', { tag: ['@win'] }, async function ({ app, python }) {
+ test.slow();
+
+ const dataFrameName = 'polars100x100';
+ await testDataExplorer(
+ app,
+ 'Python',
+ '>>>',
+ [
+ 'import polars',
+ `${dataFrameName} = polars.read_parquet("${parquetFilePath(app)}")`,
+ ],
+ dataFrameName,
+ join(app.workspacePathOrFolder, 'data-files', '100x100', 'polars-100x100.tsv')
+ );
+});
+
diff --git a/test/smoke/src/areas/positron/data-explorer/100x100-r.test.ts b/test/smoke/src/areas/positron/data-explorer/100x100-r.test.ts
new file mode 100644
index 00000000000..fcd63bef79a
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/100x100-r.test.ts
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test } from '../_test.setup';
+import { join } from 'path';
+import { parquetFilePath, testDataExplorer } from './helpers/100x100';
+
+test.use({
+ suiteId: __filename
+});
+
+test('Data Explorer 100x100 - R [C674521]', { tag: ['@win'] }, async function ({ app, r }) {
+ test.slow();
+
+ // Test the data explorer.
+ const dataFrameName = 'r100x100';
+ await testDataExplorer(
+ app,
+ 'R',
+ '>',
+ [
+ 'library(arrow)',
+ `${dataFrameName} <- read_parquet("${parquetFilePath(app)}")`,
+ ],
+ dataFrameName,
+ join(
+ app.workspacePathOrFolder,
+ 'data-files',
+ '100x100',
+ 'r-100x100.tsv'
+ )
+ );
+});
diff --git a/test/smoke/src/areas/positron/data-explorer/data-explorer-headless.test.ts b/test/smoke/src/areas/positron/data-explorer/data-explorer-headless.test.ts
new file mode 100644
index 00000000000..942b65ad6e1
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/data-explorer-headless.test.ts
@@ -0,0 +1,54 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+import { Application, Logger } from '../../../../../automation';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Headless Data Explorer - Large Data Frame', {
+ tag: ['@web']
+}, () => {
+ test.beforeEach(async function ({ app, python }) {
+ await app.workbench.positronLayouts.enterLayout('stacked');
+ });
+
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronDataExplorer.closeDataExplorer();
+ });
+
+ test('Verifies headless data explorer functionality with large parquet file [C938893]', async function ({ app, logger }) {
+ await testBody(app, logger, 'flights.parquet');
+ });
+
+ test('Verifies headless data explorer functionality with large csv file [C938894]', async function ({ app, logger }) {
+ await testBody(app, logger, 'flights.csv');
+ });
+});
+
+async function testBody(app: Application, logger: Logger, fileName: string) {
+ const LAST_CELL_CONTENTS = '2013-09-30 08:00:00';
+
+ await app.workbench.positronQuickaccess.openDataFile(join(app.workspacePathOrFolder, 'data-files', 'flights', fileName));
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ expect(await app.code.driver.getLocator(`.label-name:has-text("Data: ${fileName}")`).innerText() === `Data: ${fileName}`);
+ }).toPass();
+
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+
+ await expect(async () => {
+ // Validate full grid by checking bottom right corner data
+ await app.workbench.positronDataExplorer.clickLowerRightCorner();
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ const lastRow = tableData.at(-1);
+ const lastHour = lastRow!['time_hour'];
+ expect(lastHour).toBe(LAST_CELL_CONTENTS);
+ }).toPass();
+}
diff --git a/test/smoke/src/areas/positron/data-explorer/data-explorer-python-pandas.test.ts b/test/smoke/src/areas/positron/data-explorer/data-explorer-python-pandas.test.ts
new file mode 100644
index 00000000000..c52aab29b52
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/data-explorer-python-pandas.test.ts
@@ -0,0 +1,183 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Data Explorer - Python Pandas', {
+ tag: ['@web', '@win', '@pr']
+}, () => {
+ test('Python Pandas - Verifies basic data explorer functionality [C557556]', async function ({ app, python, logger }) {
+ // modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/
+ const script = `import pandas as pd
+data = {'Name':['Jai', 'Princi', 'Gaurav', 'Anuj'],
+ 'Age':[27, 24, 22, 32],
+ 'Address':['Delhi', 'Kanpur', 'Allahabad', 'Kannauj']}
+df = pd.DataFrame(data)`;
+
+ logger.log('Sending code to console');
+ await app.workbench.positronConsole.executeCode('Python', script, '>>>');
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('df');
+ await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText();
+ }).toPass();
+
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+
+ await expect(async () => {
+
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+
+ expect(tableData[0]).toStrictEqual({ 'Name': 'Jai', 'Age': '27', 'Address': 'Delhi' });
+ expect(tableData[1]).toStrictEqual({ 'Name': 'Princi', 'Age': '24', 'Address': 'Kanpur' });
+ expect(tableData[2]).toStrictEqual({ 'Name': 'Gaurav', 'Age': '22', 'Address': 'Allahabad' });
+ expect(tableData[3]).toStrictEqual({ 'Name': 'Anuj', 'Age': '32', 'Address': 'Kannauj' });
+ expect(tableData.length).toBe(4);
+ }).toPass({ timeout: 60000 });
+
+ await app.workbench.positronDataExplorer.closeDataExplorer();
+ await app.workbench.positronVariables.toggleVariablesView();
+
+ });
+
+ test('Python Pandas - Verifies data explorer functionality with empty fields [C718262]', async function ({ app, python, logger }) {
+ const script = `import numpy as np
+import pandas as pd
+
+data = {
+ 'A': [1, 2, np.nan, 4, 5],
+ 'B': ['foo', np.nan, 'bar', 'baz', None],
+ 'C': [np.nan, 2.5, 3.1, None, 4.8],
+ 'D': [np.nan, pd.NaT, pd.Timestamp('2023-01-01'), pd.NaT, pd.Timestamp('2023-02-01')],
+ 'E': [None, 'text', 'more text', np.nan, 'even more text']
+}
+df2 = pd.DataFrame(data)`;
+
+ logger.log('Sending code to console');
+ await app.workbench.positronConsole.executeCode('Python', script, '>>>');
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('df2');
+ await app.code.driver.getLocator('.label-name:has-text("Data: df2")').innerText();
+ }).toPass();
+
+ // Need to make sure the data explorer is visible test.beforeAll we can interact with it
+ await app.workbench.positronDataExplorer.maximizeDataExplorer(true);
+
+ await expect(async () => {
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+
+ expect(tableData[0]).toStrictEqual({ 'A': '1.00', 'B': 'foo', 'C': 'NaN', 'D': 'NaT', 'E': 'None' });
+ expect(tableData[1]).toStrictEqual({ 'A': '2.00', 'B': 'NaN', 'C': '2.50', 'D': 'NaT', 'E': 'text' });
+ expect(tableData[2]).toStrictEqual({ 'A': 'NaN', 'B': 'bar', 'C': '3.10', 'D': '2023-01-01 00:00:00', 'E': 'more text' });
+ expect(tableData[3]).toStrictEqual({ 'A': '4.00', 'B': 'baz', 'C': 'NaN', 'D': 'NaT', 'E': 'NaN' });
+ expect(tableData[4]).toStrictEqual({ 'A': '5.00', 'B': 'None', 'C': '4.80', 'D': '2023-02-01 00:00:00', 'E': 'even more text' });
+ expect(tableData.length).toBe(5);
+ }).toPass({ timeout: 60000 });
+
+ // Need to expand summary for next test
+ await app.workbench.positronDataExplorer.expandSummary();
+
+ });
+
+ // Cannot be run by itself, relies on the previous test
+ test('Python Pandas - Verifies data explorer column info functionality [C734263]', async function ({ app, python }) {
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(1)).toBe('20%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(2)).toBe('40%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(3)).toBe('40%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(4)).toBe('60%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(5)).toBe('40%');
+
+ await app.workbench.positronLayouts.enterLayout('notebook');
+
+ const col1ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(1);
+ expect(col1ProfileInfo).toStrictEqual({ 'Missing': '1', 'Min': '1.00', 'Median': '3.00', 'Mean': '3.00', 'Max': '5.00', 'SD': '1.83' });
+
+ const col2ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(2);
+ expect(col2ProfileInfo).toStrictEqual({ 'Missing': '2', 'Empty': '0', 'Unique': '3' });
+
+ const col3ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(3);
+ expect(col3ProfileInfo).toStrictEqual({ 'Missing': '2', 'Min': '2.50', 'Median': '3.10', 'Mean': '3.47', 'Max': '4.80', 'SD': '1.19' });
+
+ const col4ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(4);
+ expect(col4ProfileInfo).toStrictEqual({ 'Missing': '3', 'Min': '2023-01-01 00:00:00', 'Median': 'NaT', 'Max': '2023-02-01 00:00:00', 'Timezone': 'None' });
+
+ const col5ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(5);
+ expect(col5ProfileInfo).toStrictEqual({ 'Missing': '2', 'Empty': '0', 'Unique': '3' });
+
+ await app.workbench.positronLayouts.enterLayout('stacked');
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+
+ await app.workbench.positronDataExplorer.closeDataExplorer();
+ await app.workbench.positronVariables.toggleVariablesView();
+
+ });
+
+ // This test is not dependent on the previous test, so it refreshes the python environment
+ test('Python Pandas - Verifies data explorer test.afterAll modification [C557574]', async function ({ app, python }) {
+ // Restart python for clean environment & open the file
+ await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.positronConsole.barRestartButton.click();
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await expect(app.workbench.positronConsole.activeConsole.getByText('restarted')).toBeVisible({ timeout: 40000 });
+
+ const filename = 'pandas-update-dataframe.ipynb';
+ await app.workbench.positronNotebooks.openNotebook(join(app.workspacePathOrFolder, 'workspaces', 'data-explorer-update-datasets', filename));
+ await app.workbench.positronNotebooks.selectInterpreter('Python Environments', process.env.POSITRON_PY_VER_SEL!);
+ await app.workbench.notebook.focusFirstCell();
+ await app.workbench.notebook.executeActiveCell();
+
+ // temporary workaround for fact that variables group
+ // not properly autoselected on web
+ if (app.web) {
+ await app.workbench.positronVariables.selectVariablesGroup(filename);
+ }
+
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('df');
+ await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText();
+ }).toPass({ timeout: 50000 });
+
+ await app.workbench.positronLayouts.enterLayout('notebook');
+
+ await expect(async () => {
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ expect(tableData.length).toBe(11);
+ }).toPass({ timeout: 60000 });
+
+ await app.code.driver.getLocator('.tabs .label-name:has-text("pandas-update-dataframe.ipynb")').click();
+ await app.workbench.notebook.focusNextCell();
+ await app.workbench.notebook.executeActiveCell();
+ await app.code.driver.getLocator('.label-name:has-text("Data: df")').click();
+
+ await expect(async () => {
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ expect(tableData.length).toBe(12);
+ }).toPass({ timeout: 60000 });
+
+ await app.code.driver.getLocator('.tabs .label-name:has-text("pandas-update-dataframe.ipynb")').click();
+ await app.workbench.notebook.focusNextCell();
+ await app.workbench.notebook.executeActiveCell();
+ await app.code.driver.getLocator('.label-name:has-text("Data: df")').click();
+ await app.workbench.positronDataExplorer.selectColumnMenuItem(1, 'Sort Descending');
+
+ await expect(async () => {
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ expect(tableData[0]).toStrictEqual({ 'Year': '2025' });
+ expect(tableData.length).toBe(12);
+ }).toPass({ timeout: 60000 });
+
+ await app.workbench.positronLayouts.enterLayout('stacked');
+ });
+});
diff --git a/test/smoke/src/areas/positron/data-explorer/data-explorer-python-polars.test.ts b/test/smoke/src/areas/positron/data-explorer/data-explorer-python-polars.test.ts
new file mode 100644
index 00000000000..479baead33b
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/data-explorer-python-polars.test.ts
@@ -0,0 +1,130 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Data Explorer - Python Polars', {
+ tag: ['@win', '@web', '@pr']
+}, () => {
+ test('Python Polars - Verifies basic data explorer functionality [C644538]', async function ({ app, python, logger }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'polars-dataframe-py', 'polars_basic.py'));
+ await app.workbench.quickaccess.runCommand('python.execInConsole');
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('df');
+ await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText();
+ }).toPass();
+
+ await app.workbench.positronDataExplorer.maximizeDataExplorer(true);
+
+ await expect(async () => {
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+
+ expect(tableData[0]['foo']).toBe('1');
+ expect(tableData[1]['foo']).toBe('2');
+ expect(tableData[2]['foo']).toBe('3');
+ expect(tableData[0]['bar']).toBe('6.00');
+ expect(tableData[1]['bar']).toBe('7.00');
+ expect(tableData[2]['bar']).toBe('8.00');
+ expect(tableData[0]['ham']).toBe('2020-01-02');
+ expect(tableData[1]['ham']).toBe('2021-03-04');
+ expect(tableData[2]['ham']).toBe('2022-05-06');
+ expect(tableData.length).toBe(3);
+ }).toPass({ timeout: 60000 });
+
+ });
+
+ // Cannot be run by itself, relies on the previous test
+ test('Python Polars - Verifies basic data explorer column info functionality [C734264]', async function ({ app, python }) {
+ await app.workbench.positronDataExplorer.expandSummary();
+
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(1)).toBe('0%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(2)).toBe('0%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(3)).toBe('0%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(4)).toBe('33%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(5)).toBe('33%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(6)).toBe('33%');
+
+
+ const col1ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(1);
+ expect(col1ProfileInfo).toStrictEqual({ 'Missing': '0', 'Min': '1.00', 'Median': '2.00', 'Mean': '2.00', 'Max': '3.00', 'SD': '1.00' });
+
+ const col2ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(2);
+ expect(col2ProfileInfo).toStrictEqual({ 'Missing': '0', 'Min': '6.00', 'Median': '7.00', 'Mean': '7.00', 'Max': '8.00', 'SD': '1.00' });
+
+ const col3ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(3);
+ expect(col3ProfileInfo).toStrictEqual({ 'Missing': '0', 'Min': '2020-01-02', 'Median': '2021-03-04', 'Max': '2022-05-06' });
+
+ const col4ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(4);
+ expect(col4ProfileInfo).toStrictEqual({ 'Missing': '1', 'Min': '2.00', 'Median': '2.50', 'Mean': '2.50', 'Max': '3.00', 'SD': '0.7071' });
+
+ const col5ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(5);
+ expect(col5ProfileInfo).toStrictEqual({ 'Missing': '1', 'Min': '0.5000', 'Median': '1.50', 'Mean': '1.50', 'Max': '2.50', 'SD': '1.41' });
+
+ const col6ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(6);
+ expect(col6ProfileInfo).toStrictEqual({ 'Missing': '1', 'True': '1', 'False': '1' });
+
+ await app.workbench.positronDataExplorer.collapseSummary();
+
+ });
+
+ test('Python Polars - Add Simple Column filter [C557557]', async function ({ app, python }) {
+
+ const FILTER_PARAMS = ['foo', 'is not equal to', '1'];
+ await app.workbench.positronDataExplorer.addFilter(...FILTER_PARAMS as [string, string, string]);
+
+ await expect(async () => {
+
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+
+ expect(tableData[0]['foo']).toBe('2');
+ expect(tableData[1]['foo']).toBe('3');
+ expect(tableData[0]['bar']).toBe('7.00');
+ expect(tableData[1]['bar']).toBe('8.00');
+ expect(tableData[0]['ham']).toBe('2021-03-04');
+ expect(tableData[1]['ham']).toBe('2022-05-06');
+ expect(tableData.length).toBe(2);
+
+ }).toPass({ timeout: 60000 });
+ });
+
+ test('Python Polars - Add Simple Column Sort [C557561]', async function ({ app, python }) {
+ await app.workbench.positronDataExplorer.selectColumnMenuItem(1, 'Sort Descending');
+
+ let tableData;
+ await expect(async () => {
+ tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+
+ expect(tableData[0]['foo']).toBe('3');
+ expect(tableData[1]['foo']).toBe('2');
+ expect(tableData[0]['bar']).toBe('8.00');
+ expect(tableData[1]['bar']).toBe('7.00');
+ expect(tableData[0]['ham']).toBe('2022-05-06');
+ expect(tableData[1]['ham']).toBe('2021-03-04');
+ expect(tableData.length).toBe(2);
+ }).toPass({ timeout: 60000 });
+
+ await app.workbench.positronDataExplorer.clearSortingButton.click();
+
+ await expect(async () => {
+ tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+
+ expect(tableData[0]['foo']).toBe('2');
+ expect(tableData[1]['foo']).toBe('3');
+ expect(tableData[0]['bar']).toBe('7.00');
+ expect(tableData[1]['bar']).toBe('8.00');
+ expect(tableData[0]['ham']).toBe('2021-03-04');
+ expect(tableData[1]['ham']).toBe('2022-05-06');
+ expect(tableData.length).toBe(2);
+ }).toPass({ timeout: 60000 });
+
+ });
+});
diff --git a/test/smoke/src/areas/positron/data-explorer/data-explorer-r.test.ts b/test/smoke/src/areas/positron/data-explorer/data-explorer-r.test.ts
new file mode 100644
index 00000000000..9e82b8f8551
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/data-explorer-r.test.ts
@@ -0,0 +1,102 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Data Explorer - R ', {
+ tag: ['@web', '@win']
+}, () => {
+ test('R - Verifies basic data explorer functionality [C609620]', { tag: ['@pr'] }, async function ({ app, r, logger }) {
+ // snippet from https://www.w3schools.com/r/r_data_frames.asp
+ const script = `Data_Frame <- data.frame (
+ Training = c("Strength", "Stamina", "Other"),
+ Pulse = c(100, NA, 120),
+ Duration = c(60, 30, 45),
+ Note = c(NA, NA, "Note")
+)`;
+
+ logger.log('Sending code to console');
+ await app.workbench.positronConsole.executeCode('R', script, '>');
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('Data_Frame');
+ await app.code.driver.getLocator('.label-name:has-text("Data: Data_Frame")').innerText();
+ }).toPass();
+
+ await app.workbench.positronDataExplorer.maximizeDataExplorer(true);
+
+ await expect(async () => {
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+
+ expect(tableData[0]).toStrictEqual({ 'Training': 'Strength', 'Pulse': '100.00', 'Duration': '60.00', 'Note': 'NA' });
+ expect(tableData[1]).toStrictEqual({ 'Training': 'Stamina', 'Pulse': 'NA', 'Duration': '30.00', 'Note': 'NA' });
+ expect(tableData[2]).toStrictEqual({ 'Training': 'Other', 'Pulse': '120.00', 'Duration': '45.00', 'Note': 'Note' });
+ expect(tableData.length).toBe(3);
+ }).toPass({ timeout: 60000 });
+
+
+
+ });
+ test('R - Verifies basic data explorer column info functionality [C734265]', {
+ tag: ['@pr']
+ }, async function ({ app, r }) {
+ await app.workbench.positronDataExplorer.expandSummary();
+
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(1)).toBe('0%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(2)).toBe('33%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(3)).toBe('0%');
+ expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(4)).toBe('66%');
+
+ await app.workbench.positronLayouts.enterLayout('notebook');
+
+ const col1ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(1);
+ expect(col1ProfileInfo).toStrictEqual({ 'Missing': '0', 'Empty': '0', 'Unique': '3' });
+
+ const col2ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(2);
+ expect(col2ProfileInfo).toStrictEqual({ 'Missing': '1', 'Min': '100.00', 'Median': '110.00', 'Mean': '110.00', 'Max': '120.00', 'SD': '14.14' });
+
+ const col3ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(3);
+ expect(col3ProfileInfo).toStrictEqual({ 'Missing': '0', 'Min': '30.00', 'Median': '45.00', 'Mean': '45.00', 'Max': '60.00', 'SD': '15.00' });
+
+ const col4ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(4);
+ expect(col4ProfileInfo).toStrictEqual({ 'Missing': '2', 'Empty': '0', 'Unique': '2' });
+
+ await app.workbench.positronLayouts.enterLayout('stacked');
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+
+ await app.workbench.positronDataExplorer.closeDataExplorer();
+ await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus');
+
+ });
+
+ test('R - Open Data Explorer for the second time brings focus back [C701143]', async function ({ app, r }) {
+ // Regression test for https://github.com/posit-dev/positron/issues/4197
+ const script = `Data_Frame <- mtcars`;
+ await app.workbench.positronConsole.executeCode('R', script, '>');
+ await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus');
+
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('Data_Frame');
+ await app.code.driver.getLocator('.label-name:has-text("Data: Data_Frame")').innerText();
+ }).toPass();
+
+ // Now move focus out of the the data explorer pane
+ await app.workbench.editors.newUntitledFile();
+ await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus');
+ await app.workbench.positronVariables.doubleClickVariableRow('Data_Frame');
+
+ await expect(async () => {
+ await app.code.driver.getLocator('.label-name:has-text("Data: Data_Frame")').innerText();
+ }).toPass();
+
+ await app.workbench.positronDataExplorer.closeDataExplorer();
+ await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus');
+ });
+});
diff --git a/test/smoke/src/areas/positron/data-explorer/helpers/100x100.ts b/test/smoke/src/areas/positron/data-explorer/helpers/100x100.ts
new file mode 100644
index 00000000000..3e0f807d597
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/helpers/100x100.ts
@@ -0,0 +1,120 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import * as fs from 'fs';
+import { Application } from '../../../../../../automation';
+import { expect } from '@playwright/test';
+
+export const testDataExplorer = async (
+ app: Application,
+ language: 'Python' | 'R',
+ prompt: string,
+ commands: string[],
+ dataFrameName: string,
+ tsvFilePath: string
+): Promise => {
+ // Execute commands.
+ for (let i = 0; i < commands.length; i++) {
+ await app.workbench.positronConsole.executeCode(
+ language,
+ commands[i],
+ prompt
+ );
+ }
+
+ // Open the data frame.
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow(dataFrameName);
+ await app.code.driver.getLocator(`.label-name:has-text("Data: ${dataFrameName}")`).innerText();
+ }).toPass();
+
+ // Maximize the data explorer.
+ await app.workbench.positronDataExplorer.maximizeDataExplorer();
+
+ // Drive focus into the data explorer.
+ await app.workbench.positronDataExplorer.clickUpperLeftCorner();
+
+ // Load the TSV file that is used to verify the data and split it into lines.
+ const tsvFile = fs.readFileSync(tsvFilePath, { encoding: 'utf8' });
+ let lines: string[];
+ if (process.platform === 'win32') {
+ lines = tsvFile.split('\r\n');
+ } else {
+ lines = tsvFile.split('\n');
+ }
+
+ // Get the TSV values.
+ const tsvValues: string[][] = [];
+ for (let rowIndex = 0; rowIndex < lines.length; rowIndex++) {
+ tsvValues.push(lines[rowIndex].split('\t'));
+ }
+
+ /**
+ * Tests the row at the specified row index.
+ * @param rowIndex The row index of the row under test.
+ */
+ const testRow = async (rowIndex: number) => {
+ // Scroll to home and put the cursor there.
+ await app.workbench.positronDataExplorer.cmdCtrlHome();
+
+ // Navigate to the row under test.
+ for (let i = 0; i < rowIndex; i++) {
+ await app.workbench.positronDataExplorer.arrowDown();
+ }
+
+ // Test each cell in the row under test.
+ const row = tsvValues[rowIndex];
+ for (let columnIndex = 0; columnIndex < row.length; columnIndex++) {
+ // Get the cell.
+ const cell = await app.code.waitForElement(`#data-grid-row-cell-content-${columnIndex}-${rowIndex} .text-container .text-value`);
+
+ // Get the cell value and test value.
+ const secsRemover = (value: string) => value.replace(/^(.*)( secs)$/, '$1');
+ const cellValue = secsRemover(cell.textContent);
+ const testValue = secsRemover(row[columnIndex]);
+
+ // If the test value is a number, perform a numerical "close enough" comparison;
+ // otherwise, perform a strict equal comparison.
+ if (testValue.match(/^-?\d*\.?\d*$/)) {
+ expect(
+ Math.abs(Number.parseFloat(cellValue) - Number.parseFloat(testValue))
+ ).toBeLessThan(0.05);
+ } else {
+ expect(cell.textContent, `${rowIndex},${columnIndex}`).toStrictEqual(row[columnIndex]);
+ }
+
+ // Move to the next cell.
+ await app.workbench.positronDataExplorer.arrowRight();
+ }
+
+ };
+
+ // Check the first row, the middle row, and the last row.
+ await testRow(0);
+ await testRow(Math.trunc(tsvValues.length / 2));
+ await testRow(tsvValues.length - 1);
+
+ // Return to Stacked layout
+ await app.workbench.positronLayouts.enterLayout('stacked');
+};
+
+export const parquetFilePath = (app: Application) => {
+ // Set the path to the Parquet file.
+ let parquetFilePath = join(
+ app.workspacePathOrFolder,
+ 'data-files',
+ '100x100',
+ '100x100.parquet'
+ );
+
+ // On Windows, double escape the path.
+ if (process.platform === 'win32') {
+ parquetFilePath = parquetFilePath.replaceAll('\\', '\\\\');
+ }
+
+ // Return the path to the Parquet file.
+ return parquetFilePath;
+};
diff --git a/test/smoke/src/areas/positron/data-explorer/large-data-frame.test.ts b/test/smoke/src/areas/positron/data-explorer/large-data-frame.test.ts
new file mode 100644
index 00000000000..90c122c17e8
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/large-data-frame.test.ts
@@ -0,0 +1,92 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+
+const LAST_CELL_CONTENTS = '2013-09-30 08:00:00';
+const FILTER_PARAMS = ['distance', 'is equal to', '2586'];
+const POST_FILTER_DATA_SUMMARY = 'Showing 8,204 rows (2.44% of 336,776 total)  19 columns';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Data Explorer - Large Data Frame', {
+ tag: ['@pr', '@web', '@win']
+}, () => {
+ test.beforeEach(async function ({ app }) {
+ await app.workbench.positronLayouts.enterLayout('stacked');
+ });
+
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronDataExplorer.closeDataExplorer();
+ });
+
+ test('Python - Verifies data explorer functionality with large data frame [C557555]', async function ({ app, python, logger }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'nyc-flights-data-py', 'flights-data-frame.py'));
+ await app.workbench.quickaccess.runCommand('python.execInConsole');
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('df');
+ expect(await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText() === 'Data: df');
+ }).toPass();
+
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+
+ await expect(async () => {
+ // Validate full grid by checking bottom right corner data
+ await app.workbench.positronDataExplorer.clickLowerRightCorner();
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ const lastRow = tableData.at(-1);
+ const lastHour = lastRow!['time_hour'];
+ expect(lastHour).toBe(LAST_CELL_CONTENTS);
+ }).toPass();
+
+ await expect(async () => {
+ // Filter data set
+ await app.workbench.positronDataExplorer.clickUpperLeftCorner();
+ await app.workbench.positronDataExplorer.addFilter(...FILTER_PARAMS as [string, string, string]);
+
+ const statusBar = await app.workbench.positronDataExplorer.getDataExplorerStatusBar();
+ expect(statusBar.textContent).toBe(POST_FILTER_DATA_SUMMARY);
+ }).toPass();
+
+ });
+
+ test('R - Verifies data explorer functionality with large data frame [C557554]', {
+ tag: ['@web', '@pr']
+ }, async function ({ app, logger, r }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'nyc-flights-data-r', 'flights-data-frame.r'));
+ await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('df2');
+ expect(await app.code.driver.getLocator('.label-name:has-text("Data: df2")').innerText() === 'Data: df2');
+ }).toPass();
+
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+
+ await expect(async () => {
+ // Validate full grid by checking bottom right corner data
+ await app.workbench.positronDataExplorer.clickLowerRightCorner();
+ const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ const lastRow = tableData.at(-1);
+ const lastHour = lastRow!['time_hour'];
+ expect(lastHour).toBe(LAST_CELL_CONTENTS);
+ }).toPass();
+
+ await expect(async () => {
+ // Filter data set
+ await app.workbench.positronDataExplorer.clickUpperLeftCorner();
+ await app.workbench.positronDataExplorer.addFilter(...FILTER_PARAMS as [string, string, string]);
+ const statusBar = await app.workbench.positronDataExplorer.getDataExplorerStatusBar();
+ expect(statusBar.textContent).toBe(POST_FILTER_DATA_SUMMARY);
+ }).toPass();
+
+ });
+});
diff --git a/test/smoke/src/areas/positron/data-explorer/sparklines.test.ts b/test/smoke/src/areas/positron/data-explorer/sparklines.test.ts
new file mode 100644
index 00000000000..6228cba3e5a
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/sparklines.test.ts
@@ -0,0 +1,82 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { Application } from '../../../../../automation';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Data Explorer - Sparklines', {
+ tag: ['@web', '@win']
+}, () => {
+
+ test.beforeEach(async function ({ app }) {
+ await app.workbench.positronLayouts.enterLayout('stacked');
+ });
+
+ test.afterEach(async ({ app }) => {
+ await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
+ });
+
+ test('Python Pandas - Verifies downward trending graph [C830552]', async ({ app, python }) => {
+ await app.workbench.positronConsole.executeCode('Python', pythonScript, '>>>');
+ await openDataExplorerColumnProfile(app, 'pythonData');
+ await verifyGraphBarHeights(app);
+ });
+
+
+ test('R - Verifies downward trending graph [C830553]', async ({ app, r }) => {
+ await app.workbench.positronConsole.executeCode('R', rScript, '>');
+ await openDataExplorerColumnProfile(app, 'rData');
+ await verifyGraphBarHeights(app);
+ });
+});
+
+async function openDataExplorerColumnProfile(app: Application, variableName: string) {
+
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow(variableName);
+ await app.code.driver.getLocator(`.label-name:has-text("Data: ${variableName}")`).innerText();
+ }).toPass();
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+ await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ await app.workbench.positronDataExplorer.expandColumnProfile(0);
+}
+
+async function verifyGraphBarHeights(app: Application) {
+ // Get all graph graph bars/rectangles
+ await expect(async () => {
+ const rects = app.code.driver.getLocator('rect.count');
+
+ // Iterate over each rect and verify the height
+ const expectedHeights = ['50', '40', '30', '20', '10'];
+ for (let i = 0; i < expectedHeights.length; i++) {
+ const height = await rects.nth(i).getAttribute('height');
+ expect(height).toBe(expectedHeights[i]);
+ }
+ }).toPass({ timeout: 10000 });
+}
+
+
+const rScript = `library(dplyr)
+
+rData <- tibble(
+category = c("A", "A", "A", "A", "B", "B", "B", "C", "C", "D", "E", "A", "B", "C", "D"),
+values = c(1, 2, 3, 4, 5, 9, 10, 11, 13, 25, 7, 15, 20, 5, 6)
+)`;
+
+
+const pythonScript = `import pandas as pd
+import matplotlib.pyplot as plt
+
+pythonData = pd.DataFrame({
+'category': ['A', 'A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'D', 'E', 'A', 'B', 'C', 'D'],
+'values': [1, 2, 3, 4, 5, 9, 10, 11, 13, 25, 7, 15, 20, 5, 6]
+})`;
+
diff --git a/test/smoke/src/areas/positron/dataexplorer/veryLargeDataFrame.test.ts b/test/smoke/src/areas/positron/data-explorer/very-large-data-frame.test.ts
similarity index 71%
rename from test/smoke/src/areas/positron/dataexplorer/veryLargeDataFrame.test.ts
rename to test/smoke/src/areas/positron/data-explorer/very-large-data-frame.test.ts
index a00c7b16eda..4b866257f78 100644
--- a/test/smoke/src/areas/positron/dataexplorer/veryLargeDataFrame.test.ts
+++ b/test/smoke/src/areas/positron/data-explorer/very-large-data-frame.test.ts
@@ -4,13 +4,15 @@
*--------------------------------------------------------------------------------------------*/
-import { expect } from '@playwright/test';
-import { Application, downloadFileFromS3, PositronPythonFixtures, PositronRFixtures, S3FileDownloadOptions } from '../../../../../automation';
+
import { fail } from 'assert';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
import { join } from 'path';
+import { downloadFileFromS3, S3FileDownloadOptions } from '../../../../../automation';
+import { test, expect } from '../_test.setup';
-let logger;
+test.use({
+ suiteId: __filename
+});
// AWS Configuration
const region = "us-west-2";
@@ -19,15 +21,10 @@ const objectKey = "largeParquet.parquet";
const githubActions = process.env.GITHUB_ACTIONS === "true";
-describe('Data Explorer - Very Large Data Frame #win', () => {
- logger = setupAndStartApp();
-
- before(async function () {
-
+test.describe('Data Explorer - Very Large Data Frame', { tag: ['@win'] }, () => {
+ test.beforeAll(async function ({ app }) {
if (githubActions) {
-
- const localFilePath = join(this.app.workspacePathOrFolder, "data-files", objectKey);
-
+ const localFilePath = join(app.workspacePathOrFolder, "data-files", objectKey);
const downloadOptions: S3FileDownloadOptions = {
region: region,
bucketName: bucketName,
@@ -36,34 +33,19 @@ describe('Data Explorer - Very Large Data Frame #win', () => {
};
await downloadFileFromS3(downloadOptions);
}
-
});
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronDataExplorer.closeDataExplorer();
+ await app.workbench.positronVariables.toggleVariablesView();
+ });
- describe('Python Data Explorer (Very Large Data Frame)', () => {
-
- before(async function () {
-
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
- });
-
- afterEach(async function () {
-
- const app = this.app as Application;
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- await app.workbench.positronVariables.toggleVariablesView();
-
- });
+ test.describe('Python Data Explorer (Very Large Data Frame)', () => {
if (githubActions) {
- it('Python - Verifies data explorer functionality with very large unique data dataframe [C804823]', async function () {
- const app = this.app as Application;
+ test('Python - Verifies data explorer functionality with very large unique data dataframe [C804823]', async function ({ app, logger, python }) {
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'performance', 'loadBigParquet.py'));
-
await app.workbench.quickaccess.runCommand('python.execInConsole');
-
const startTime = performance.now();
logger.log('Opening data grid');
@@ -73,12 +55,9 @@ describe('Data Explorer - Very Large Data Frame #win', () => {
}).toPass();
await app.workbench.positronSideBar.closeSecondarySideBar();
-
// awaits table load completion
await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
const endTime = performance.now();
-
const timeTaken = endTime - startTime;
if (timeTaken > 40000) {
@@ -86,16 +65,12 @@ describe('Data Explorer - Very Large Data Frame #win', () => {
} else {
logger.log(`Opening large unique parquet took ${timeTaken} milliseconds (pandas)`);
}
-
});
} else {
- it('Python - Verifies data explorer functionality with very large duplicated data dataframe [C807824]', async function () {
- const app = this.app as Application;
+ test('Python - Verifies data explorer functionality with very large duplicated data dataframe [C807824]', async function ({ app, logger, python }) {
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'performance', 'multiplyParquet.py'));
-
await app.workbench.quickaccess.runCommand('python.execInConsole');
-
const startTime = performance.now();
logger.log('Opening data grid');
@@ -108,9 +83,7 @@ describe('Data Explorer - Very Large Data Frame #win', () => {
// awaits table load completion
await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
const endTime = performance.now();
-
const timeTaken = endTime - startTime;
if (timeTaken > 27000) {
@@ -122,30 +95,11 @@ describe('Data Explorer - Very Large Data Frame #win', () => {
}
});
- describe('R Data Explorer (Very Large Data Frame)', () => {
-
- before(async function () {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- });
-
- afterEach(async function () {
-
- const app = this.app as Application;
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- await app.workbench.positronVariables.toggleVariablesView();
-
- });
-
+ test.describe('R Data Explorer (Very Large Data Frame)', () => {
if (githubActions) {
- it('R - Verifies data explorer functionality with very large unique data dataframe [C804824]', async function () {
- const app = this.app as Application;
+ test('R - Verifies data explorer functionality with very large unique data dataframe [C804824]', async function ({ app, logger, r }) {
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'performance', 'loadBigParquet.r'));
-
await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
-
const startTime = performance.now();
logger.log('Opening data grid');
@@ -158,9 +112,7 @@ describe('Data Explorer - Very Large Data Frame #win', () => {
// awaits table load completion
await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
const endTime = performance.now();
-
const timeTaken = endTime - startTime;
if (timeTaken > 75000) {
@@ -171,12 +123,9 @@ describe('Data Explorer - Very Large Data Frame #win', () => {
});
} else {
- it('R - Verifies data explorer functionality with very large duplicated data dataframe [C807825]', async function () {
- const app = this.app as Application;
+ test('R - Verifies data explorer functionality with very large duplicated data dataframe [C807825]', async function ({ app, logger, r }) {
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'performance', 'multiplyParquet.r'));
-
await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
-
const startTime = performance.now();
logger.log('Opening data grid');
@@ -189,9 +138,7 @@ describe('Data Explorer - Very Large Data Frame #win', () => {
// awaits table load completion
await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
const endTime = performance.now();
-
const timeTaken = endTime - startTime;
if (timeTaken > 60000) {
diff --git a/test/smoke/src/areas/positron/data-explorer/xlsx-data-frame.test.ts b/test/smoke/src/areas/positron/data-explorer/xlsx-data-frame.test.ts
new file mode 100644
index 00000000000..5af6398a014
--- /dev/null
+++ b/test/smoke/src/areas/positron/data-explorer/xlsx-data-frame.test.ts
@@ -0,0 +1,55 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Data Explorer - XLSX', {
+ tag: ['@web', '@win']
+}, () => {
+
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronDataExplorer.closeDataExplorer();
+ await app.workbench.positronVariables.toggleVariablesView();
+ });
+
+ test('Python - Verifies data explorer functionality with XLSX input [C632940]', async function ({ app, python, logger }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'read-xlsx-py', 'supermarket-sales.py'));
+ await app.workbench.quickaccess.runCommand('python.execInConsole');
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('df');
+ await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText();
+ }).toPass();
+
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+ await app.workbench.positronDataExplorer.selectColumnMenuItem(1, 'Sort Descending');
+ const visibleTableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ const firstRow = visibleTableData.at(0);
+ expect(firstRow!['Invoice ID']).toBe('898-04-2717');
+ });
+
+ test('R - Verifies data explorer functionality with XLSX input [C632941]', async function ({ app, r, logger }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'read-xlsx-r', 'supermarket-sales.r'));
+ await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
+
+ logger.log('Opening data grid');
+ await expect(async () => {
+ await app.workbench.positronVariables.doubleClickVariableRow('df2');
+ await app.code.driver.getLocator('.label-name:has-text("Data: df2")').innerText();
+ }).toPass();
+
+ await app.workbench.positronSideBar.closeSecondarySideBar();
+ await app.workbench.positronDataExplorer.selectColumnMenuItem(1, 'Sort Descending');
+ const visibleTableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
+ const firstRow = visibleTableData.at(0);
+ expect(firstRow!['Invoice ID']).toBe('898-04-2717');
+ });
+});
diff --git a/test/smoke/src/areas/positron/dataexplorer/data-explorer-100x100.test.ts b/test/smoke/src/areas/positron/dataexplorer/data-explorer-100x100.test.ts
deleted file mode 100644
index c51413a0aa6..00000000000
--- a/test/smoke/src/areas/positron/dataexplorer/data-explorer-100x100.test.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import * as fs from 'fs';
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-import { join } from 'path';
-
-describe('Data Explorer 100x100 #win', function () {
- setupAndStartApp();
-
- /**
- * Tests the data explorer.
- * @param app The application.
- * @param language The language.
- * @param prompt The prompt.
- * @param commands Commands to run to set up the test.
- * @param dataFrameName The data frame name.
- * @param tsvFilePath The TSV file path.
- */
- const testDataExplorer = async (
- app: Application,
- language: 'Python' | 'R',
- prompt: string,
- commands: string[],
- dataFrameName: string,
- tsvFilePath: string
- ): Promise => {
- // Execute commands.
- for (let i = 0; i < commands.length; i++) {
- await app.workbench.positronConsole.executeCode(
- language,
- commands[i],
- prompt
- );
- }
-
- // Open the data frame.
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow(dataFrameName);
- await app.code.driver.getLocator(`.label-name:has-text("Data: ${dataFrameName}")`).innerText();
- }).toPass();
-
- // Maximize the data explorer.
- await app.workbench.positronDataExplorer.maximizeDataExplorer();
-
- // Drive focus into the data explorer.
- await app.workbench.positronDataExplorer.clickUpperLeftCorner();
-
- // Load the TSV file that is used to verify the data and split it into lines.
- const tsvFile = fs.readFileSync(tsvFilePath, { encoding: 'utf8' });
- let lines: string[];
- if (process.platform === 'win32') {
- lines = tsvFile.split('\r\n');
- } else {
- lines = tsvFile.split('\n');
- }
-
- // Get the TSV values.
- const tsvValues: string[][] = [];
- for (let rowIndex = 0; rowIndex < lines.length; rowIndex++) {
- tsvValues.push(lines[rowIndex].split('\t'));
- }
-
- /**
- * Tests the row at the specified row index.
- * @param rowIndex The row index of the row under test.
- */
- const testRow = async (rowIndex: number) => {
- // Scroll to home and put the cursor there.
- await app.workbench.positronDataExplorer.cmdCtrlHome();
-
- // Navigate to the row under test.
- for (let i = 0; i < rowIndex; i++) {
- await app.workbench.positronDataExplorer.arrowDown();
- }
-
- // Test each cell in the row under test.
- const row = tsvValues[rowIndex];
- for (let columnIndex = 0; columnIndex < row.length; columnIndex++) {
- // Get the cell.
- const cell = await app.code.waitForElement(`#data-grid-row-cell-content-${columnIndex}-${rowIndex} .text-container .text-value`);
-
- // Get the cell value and test value.
- const secsRemover = (value: string) => value.replace(/^(.*)( secs)$/, '$1');
- const cellValue = secsRemover(cell.textContent);
- const testValue = secsRemover(row[columnIndex]);
-
- // If the test value is a number, perform a numerical "close enough" comparison;
- // otherwise, perform a strict equal comparison.
- if (testValue.match(/^-?\d*\.?\d*$/)) {
- expect(
- Math.abs(Number.parseFloat(cellValue) - Number.parseFloat(testValue))
- ).toBeLessThan(0.05);
- } else {
- expect(cell.textContent, `${rowIndex},${columnIndex}`).toStrictEqual(row[columnIndex]);
- }
-
- // Move to the next cell.
- await app.workbench.positronDataExplorer.arrowRight();
- }
-
- };
-
- // Check the first row, the middle row, and the last row.
- await testRow(0);
- await testRow(Math.trunc(tsvValues.length / 2));
- await testRow(tsvValues.length - 1);
-
- // Return to Stacked layout
- await app.workbench.positronLayouts.enterLayout('stacked');
- };
-
- /**
- * Constructs the Parquet file path.
- * @param app The application.
- * @returns The Parquet file path.
- */
- const parquetFilePath = (app: Application) => {
- // Set the path to the Parquet file.
- let parquetFilePath = join(
- app.workspacePathOrFolder,
- 'data-files',
- '100x100',
- '100x100.parquet'
- );
-
- // On Windows, double escape the path.
- if (process.platform === 'win32') {
- parquetFilePath = parquetFilePath.replaceAll('\\', '\\\\');
- }
-
- // Return the path to the Parquet file.
- return parquetFilePath;
- };
-
- /**
- * Data Explorer 100x100 - Python - Pandas.
- */
- describe('Data Explorer 100x100 - Python - Pandas', function () {
- /**
- * Before hook.
- */
- before(async function () {
- const app = this.app as Application;
- const pythonFixtures = new PositronPythonFixtures(app);
- await pythonFixtures.startPythonInterpreter();
- });
-
- /**
- * After hook.
- */
- after(async function () {
- const app = this.app as Application;
- await app.workbench.positronDataExplorer.closeDataExplorer();
- });
-
- /**
- * Data Explorer 100x100 - Python - Pandas - Smoke Test.
- */
- it('Data Explorer 100x100 - Python - Pandas - Smoke Test [C557563]', async function () {
- // Get the app.
- const app = this.app as Application;
- this.timeout(180000);
-
- // Test the data explorer.
- const dataFrameName = 'pandas100x100';
- await testDataExplorer(
- app,
- 'Python',
- '>>>',
- [
- 'import pandas as pd',
- `${dataFrameName} = pd.read_parquet("${parquetFilePath(app)}")`,
- ],
- dataFrameName,
- join(app.workspacePathOrFolder, 'data-files', '100x100', 'pandas-100x100.tsv')
- );
- });
- });
-
- /**
- * Data Explorer 100x100 - Python - Polars.
- */
- describe('Data Explorer 100x100 - Python - Polars', function () {
- /**
- * Before hook.
- */
- before(async function () {
- const app = this.app as Application;
- const pythonFixtures = new PositronPythonFixtures(app);
- await pythonFixtures.startPythonInterpreter();
- });
-
- /**
- * After hook.
- */
- after(async function () {
- const app = this.app as Application;
- await app.workbench.positronDataExplorer.closeDataExplorer();
- });
-
- /**
- * Data Explorer 100x100 - Python - Polars - Smoke Test.
- */
- it('Data Explorer 100x100 - Python - Polars - Smoke Test [C674520]', async function () {
- // Get the app.
- const app = this.app as Application;
- this.timeout(180000);
-
- // Test the data explorer.
- const dataFrameName = 'polars100x100';
- await testDataExplorer(
- app,
- 'Python',
- '>>>',
- [
- 'import polars',
- `${dataFrameName} = polars.read_parquet("${parquetFilePath(app)}")`,
- ],
- dataFrameName,
- join(app.workspacePathOrFolder, 'data-files', '100x100', 'polars-100x100.tsv')
- );
- });
- });
-
- /**
- * Data Explorer 100x100 - R.
- */
- describe('Data Explorer 100x100 - R', function () {
- /**
- * Before hook.
- */
- before(async function () {
- const app = this.app as Application;
- const rFixtures = new PositronRFixtures(app);
- await rFixtures.startRInterpreter();
- });
-
- /**
- * After hook.
- */
- after(async function () {
- const app = this.app as Application;
- await app.workbench.positronDataExplorer.closeDataExplorer();
- });
-
- /**
- * Data Explorer 100x100 - R - Smoke Test.
- */
- it('Data Explorer 100x100 - R - Smoke Test [C674521]', async function () {
- // Get the app.
- const app = this.app as Application;
- this.timeout(180000);
-
- // Test the data explorer.
- const dataFrameName = 'r100x100';
- await testDataExplorer(
- app,
- 'R',
- '>',
- [
- 'library(arrow)',
- `${dataFrameName} <- read_parquet("${parquetFilePath(app)}")`,
- ],
- dataFrameName,
- join(
- app.workspacePathOrFolder,
- 'data-files',
- '100x100',
- 'r-100x100.tsv'
- )
- );
- });
- });
-});
diff --git a/test/smoke/src/areas/positron/dataexplorer/dataexplorer.test.ts b/test/smoke/src/areas/positron/dataexplorer/dataexplorer.test.ts
deleted file mode 100644
index 91f84c05b60..00000000000
--- a/test/smoke/src/areas/positron/dataexplorer/dataexplorer.test.ts
+++ /dev/null
@@ -1,471 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { join } from 'path';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-let logger;
-
-describe('Data Explorer #web #win', () => {
- logger = setupAndStartApp();
-
- describe('Python Pandas Data Explorer #pr', () => {
-
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- after(async function () {
-
- const app = this.app as Application;
-
- await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barRestartButton.click();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
-
- });
-
- it('Python Pandas - Verifies basic data explorer functionality [C557556]', async function () {
- const app = this.app as Application;
- this.timeout(120000);
-
- // modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/
- const script = `import pandas as pd
-data = {'Name':['Jai', 'Princi', 'Gaurav', 'Anuj'],
- 'Age':[27, 24, 22, 32],
- 'Address':['Delhi', 'Kanpur', 'Allahabad', 'Kannauj']}
-df = pd.DataFrame(data)`;
-
- logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('Python', script, '>>>');
-
- logger.log('Opening data grid');
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('df');
- await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText();
- }).toPass();
-
- await app.workbench.positronSideBar.closeSecondarySideBar();
-
- await expect(async () => {
-
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- expect(tableData[0]).toStrictEqual({ 'Name': 'Jai', 'Age': '27', 'Address': 'Delhi' });
- expect(tableData[1]).toStrictEqual({ 'Name': 'Princi', 'Age': '24', 'Address': 'Kanpur' });
- expect(tableData[2]).toStrictEqual({ 'Name': 'Gaurav', 'Age': '22', 'Address': 'Allahabad' });
- expect(tableData[3]).toStrictEqual({ 'Name': 'Anuj', 'Age': '32', 'Address': 'Kannauj' });
- expect(tableData.length).toBe(4);
- }).toPass({ timeout: 60000 });
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- await app.workbench.positronVariables.toggleVariablesView();
-
- });
-
- it('Python Pandas - Verifies data explorer functionality with empty fields [C718262]', async function () {
- const app = this.app as Application;
- this.timeout(120000);
-
- const script = `import numpy as np
-import pandas as pd
-
-data = {
- 'A': [1, 2, np.nan, 4, 5],
- 'B': ['foo', np.nan, 'bar', 'baz', None],
- 'C': [np.nan, 2.5, 3.1, None, 4.8],
- 'D': [np.nan, pd.NaT, pd.Timestamp('2023-01-01'), pd.NaT, pd.Timestamp('2023-02-01')],
- 'E': [None, 'text', 'more text', np.nan, 'even more text']
-}
-df2 = pd.DataFrame(data)`;
-
- logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('Python', script, '>>>');
-
- logger.log('Opening data grid');
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('df2');
- await app.code.driver.getLocator('.label-name:has-text("Data: df2")').innerText();
- }).toPass();
-
- // Need to make sure the data explorer is visible before we can interact with it
- await app.workbench.positronDataExplorer.maximizeDataExplorer(true);
-
- await expect(async () => {
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- expect(tableData[0]).toStrictEqual({ 'A': '1.00', 'B': 'foo', 'C': 'NaN', 'D': 'NaT', 'E': 'None' });
- expect(tableData[1]).toStrictEqual({ 'A': '2.00', 'B': 'NaN', 'C': '2.50', 'D': 'NaT', 'E': 'text' });
- expect(tableData[2]).toStrictEqual({ 'A': 'NaN', 'B': 'bar', 'C': '3.10', 'D': '2023-01-01 00:00:00', 'E': 'more text' });
- expect(tableData[3]).toStrictEqual({ 'A': '4.00', 'B': 'baz', 'C': 'NaN', 'D': 'NaT', 'E': 'NaN' });
- expect(tableData[4]).toStrictEqual({ 'A': '5.00', 'B': 'None', 'C': '4.80', 'D': '2023-02-01 00:00:00', 'E': 'even more text' });
- expect(tableData.length).toBe(5);
- }).toPass({ timeout: 60000 });
-
- // Need to expand summary for next test
- await app.workbench.positronDataExplorer.expandSummary();
-
- });
- // Cannot be run by itself, relies on the previous test
- it('Python Pandas - Verifies data explorer column info functionality [C734263]', async function () {
-
- const app = this.app as Application;
- this.timeout(120000);
-
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(1)).toBe('20%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(2)).toBe('40%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(3)).toBe('40%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(4)).toBe('60%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(5)).toBe('40%');
-
- await app.workbench.positronLayouts.enterLayout('notebook');
-
- const col1ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(1);
- expect(col1ProfileInfo).toStrictEqual({ 'Missing': '1', 'Min': '1.00', 'Median': '3.00', 'Mean': '3.00', 'Max': '5.00', 'SD': '1.83' });
-
- const col2ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(2);
- expect(col2ProfileInfo).toStrictEqual({ 'Missing': '2', 'Empty': '0', 'Unique': '3' });
-
- const col3ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(3);
- expect(col3ProfileInfo).toStrictEqual({ 'Missing': '2', 'Min': '2.50', 'Median': '3.10', 'Mean': '3.47', 'Max': '4.80', 'SD': '1.19' });
-
- const col4ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(4);
- expect(col4ProfileInfo).toStrictEqual({ 'Missing': '3', 'Min': '2023-01-01 00:00:00', 'Median': 'NaT', 'Max': '2023-02-01 00:00:00', 'Timezone': 'None' });
-
- const col5ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(5);
- expect(col5ProfileInfo).toStrictEqual({ 'Missing': '2', 'Empty': '0', 'Unique': '3' });
-
- await app.workbench.positronLayouts.enterLayout('stacked');
- await app.workbench.positronSideBar.closeSecondarySideBar();
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- await app.workbench.positronVariables.toggleVariablesView();
-
- });
- // This test is not dependent on the previous test, so it refreshes the python environment
- it('Python Pandas - Verifies data explorer after modification [C557574]', async function () {
-
- const app = this.app as Application;
- this.timeout(120000);
-
- // Restart python for clean environment & open the file
- await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.positronConsole.barRestartButton.click();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
-
- const filename = 'pandas-update-dataframe.ipynb';
- await app.workbench.positronNotebooks.openNotebook(join(app.workspacePathOrFolder, 'workspaces', 'data-explorer-update-datasets', filename));
- await app.workbench.notebook.focusFirstCell();
- await app.workbench.notebook.executeActiveCell();
-
- // temporary workaround for fact that variables group
- // not properly autoselected on web
- if (app.web) {
- await app.workbench.positronVariables.selectVariablesGroup(filename);
- }
-
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('df');
- await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText();
- }).toPass({ timeout: 50000 });
-
- await app.workbench.positronLayouts.enterLayout('notebook');
-
- await expect(async () => {
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
- expect(tableData.length).toBe(11);
- }).toPass({ timeout: 60000 });
-
- await app.code.driver.getLocator('.tabs .label-name:has-text("pandas-update-dataframe.ipynb")').click();
- await app.workbench.notebook.focusNextCell();
- await app.workbench.notebook.executeActiveCell();
- await app.code.driver.getLocator('.label-name:has-text("Data: df")').click();
-
- await expect(async () => {
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
- expect(tableData.length).toBe(12);
- }).toPass({ timeout: 60000 });
-
- await app.code.driver.getLocator('.tabs .label-name:has-text("pandas-update-dataframe.ipynb")').click();
- await app.workbench.notebook.focusNextCell();
- await app.workbench.notebook.executeActiveCell();
- await app.code.driver.getLocator('.label-name:has-text("Data: df")').click();
- await app.workbench.positronDataExplorer.selectColumnMenuItem(1, 'Sort Descending');
-
- await expect(async () => {
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
- expect(tableData[0]).toStrictEqual({ 'Year': '2025' });
- expect(tableData.length).toBe(12);
- }).toPass({ timeout: 60000 });
-
- await app.workbench.positronLayouts.enterLayout('stacked');
- });
- });
-
-});
-
-
-describe('Data Explorer #web #win', () => {
- logger = setupAndStartApp();
-
- describe('Python Polars Data Explorer #pr', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- after(async function () {
-
- const app = this.app as Application;
-
- await app.workbench.positronLayouts.enterLayout('stacked');
- await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
-
- });
-
- it('Python Polars - Verifies basic data explorer functionality [C644538]', async function () {
- const app = this.app as Application;
- this.timeout(120000);
-
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'polars-dataframe-py', 'polars_basic.py'));
- await app.workbench.quickaccess.runCommand('python.execInConsole');
-
- logger.log('Opening data grid');
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('df');
- await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText();
- }).toPass();
-
- await app.workbench.positronDataExplorer.maximizeDataExplorer(true);
-
- await expect(async () => {
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- expect(tableData[0]['foo']).toBe('1');
- expect(tableData[1]['foo']).toBe('2');
- expect(tableData[2]['foo']).toBe('3');
- expect(tableData[0]['bar']).toBe('6.00');
- expect(tableData[1]['bar']).toBe('7.00');
- expect(tableData[2]['bar']).toBe('8.00');
- expect(tableData[0]['ham']).toBe('2020-01-02');
- expect(tableData[1]['ham']).toBe('2021-03-04');
- expect(tableData[2]['ham']).toBe('2022-05-06');
- expect(tableData.length).toBe(3);
- }).toPass({ timeout: 60000 });
-
- });
- // Cannot be run by itself, relies on the previous test
- it('Python Polars - Verifies basic data explorer column info functionality [C734264]', async function () {
-
- const app = this.app as Application;
- this.timeout(120000);
-
- await app.workbench.positronDataExplorer.expandSummary();
-
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(1)).toBe('0%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(2)).toBe('0%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(3)).toBe('0%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(4)).toBe('33%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(5)).toBe('33%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(6)).toBe('33%');
-
-
- const col1ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(1);
- expect(col1ProfileInfo).toStrictEqual({ 'Missing': '0', 'Min': '1.00', 'Median': '2.00', 'Mean': '2.00', 'Max': '3.00', 'SD': '1.00' });
-
- const col2ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(2);
- expect(col2ProfileInfo).toStrictEqual({ 'Missing': '0', 'Min': '6.00', 'Median': '7.00', 'Mean': '7.00', 'Max': '8.00', 'SD': '1.00' });
-
- const col3ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(3);
- expect(col3ProfileInfo).toStrictEqual({ 'Missing': '0', 'Min': '2020-01-02', 'Median': '2021-03-04', 'Max': '2022-05-06' });
-
- const col4ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(4);
- expect(col4ProfileInfo).toStrictEqual({ 'Missing': '1', 'Min': '2.00', 'Median': '2.50', 'Mean': '2.50', 'Max': '3.00', 'SD': '0.7071' });
-
- const col5ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(5);
- expect(col5ProfileInfo).toStrictEqual({ 'Missing': '1', 'Min': '0.5000', 'Median': '1.50', 'Mean': '1.50', 'Max': '2.50', 'SD': '1.41' });
-
- const col6ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(6);
- expect(col6ProfileInfo).toStrictEqual({ 'Missing': '1', 'True': '1', 'False': '1' });
-
- await app.workbench.positronDataExplorer.collapseSummary();
-
- });
-
- it('Python Polars - Add Simple Column filter [C557557]', async function () {
- const app = this.app as Application;
- this.timeout(120000);
-
- const FILTER_PARAMS = ['foo', 'is not equal to', '1'];
- await app.workbench.positronDataExplorer.addFilter(...FILTER_PARAMS as [string, string, string]);
-
- await expect(async () => {
-
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- expect(tableData[0]['foo']).toBe('2');
- expect(tableData[1]['foo']).toBe('3');
- expect(tableData[0]['bar']).toBe('7.00');
- expect(tableData[1]['bar']).toBe('8.00');
- expect(tableData[0]['ham']).toBe('2021-03-04');
- expect(tableData[1]['ham']).toBe('2022-05-06');
- expect(tableData.length).toBe(2);
-
- }).toPass({ timeout: 60000 });
- });
-
- it('Python Polars - Add Simple Column Sort [C557561]', async function () {
- const app = this.app as Application;
- this.timeout(120000);
-
- await app.workbench.positronDataExplorer.selectColumnMenuItem(1, 'Sort Descending');
-
- let tableData;
- await expect(async () => {
- tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- expect(tableData[0]['foo']).toBe('3');
- expect(tableData[1]['foo']).toBe('2');
- expect(tableData[0]['bar']).toBe('8.00');
- expect(tableData[1]['bar']).toBe('7.00');
- expect(tableData[0]['ham']).toBe('2022-05-06');
- expect(tableData[1]['ham']).toBe('2021-03-04');
- expect(tableData.length).toBe(2);
- }).toPass({ timeout: 60000 });
-
- await app.workbench.positronDataExplorer.clearSortingButton.click();
-
- await expect(async () => {
- tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- expect(tableData[0]['foo']).toBe('2');
- expect(tableData[1]['foo']).toBe('3');
- expect(tableData[0]['bar']).toBe('7.00');
- expect(tableData[1]['bar']).toBe('8.00');
- expect(tableData[0]['ham']).toBe('2021-03-04');
- expect(tableData[1]['ham']).toBe('2022-05-06');
- expect(tableData.length).toBe(2);
- }).toPass({ timeout: 60000 });
-
- });
- });
-
-});
-
-
-describe('Data Explorer #web #win', () => {
- logger = setupAndStartApp();
-
- describe('R Data Explorer', () => {
-
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- it('R - Verifies basic data explorer functionality [C609620] #pr', async function () {
- const app = this.app as Application;
- this.timeout(120000);
-
- // snippet from https://www.w3schools.com/r/r_data_frames.asp
- const script = `Data_Frame <- data.frame (
- Training = c("Strength", "Stamina", "Other"),
- Pulse = c(100, NA, 120),
- Duration = c(60, 30, 45),
- Note = c(NA, NA, "Note")
-)`;
-
- logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('R', script, '>');
-
- logger.log('Opening data grid');
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('Data_Frame');
- await app.code.driver.getLocator('.label-name:has-text("Data: Data_Frame")').innerText();
- }).toPass();
-
- await app.workbench.positronDataExplorer.maximizeDataExplorer(true);
-
- await expect(async () => {
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- expect(tableData[0]).toStrictEqual({ 'Training': 'Strength', 'Pulse': '100.00', 'Duration': '60.00', 'Note': 'NA' });
- expect(tableData[1]).toStrictEqual({ 'Training': 'Stamina', 'Pulse': 'NA', 'Duration': '30.00', 'Note': 'NA' });
- expect(tableData[2]).toStrictEqual({ 'Training': 'Other', 'Pulse': '120.00', 'Duration': '45.00', 'Note': 'Note' });
- expect(tableData.length).toBe(3);
- }).toPass({ timeout: 60000 });
-
-
-
- });
- it('R - Verifies basic data explorer column info functionality [C734265] #pr', async function () {
-
- const app = this.app as Application;
- this.timeout(120000);
-
- await app.workbench.positronDataExplorer.expandSummary();
-
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(1)).toBe('0%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(2)).toBe('33%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(3)).toBe('0%');
- expect(await app.workbench.positronDataExplorer.getColumnMissingPercent(4)).toBe('66%');
-
- await app.workbench.positronLayouts.enterLayout('notebook');
-
- const col1ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(1);
- expect(col1ProfileInfo).toStrictEqual({ 'Missing': '0', 'Empty': '0', 'Unique': '3' });
-
- const col2ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(2);
- expect(col2ProfileInfo).toStrictEqual({ 'Missing': '1', 'Min': '100.00', 'Median': '110.00', 'Mean': '110.00', 'Max': '120.00', 'SD': '14.14' });
-
- const col3ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(3);
- expect(col3ProfileInfo).toStrictEqual({ 'Missing': '0', 'Min': '30.00', 'Median': '45.00', 'Mean': '45.00', 'Max': '60.00', 'SD': '15.00' });
-
- const col4ProfileInfo = await app.workbench.positronDataExplorer.getColumnProfileInfo(4);
- expect(col4ProfileInfo).toStrictEqual({ 'Missing': '2', 'Empty': '0', 'Unique': '2' });
-
- await app.workbench.positronLayouts.enterLayout('stacked');
- await app.workbench.positronSideBar.closeSecondarySideBar();
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus');
-
- });
-
- it('R - Open Data Explorer for the second time brings focus back [C701143]', async function () {
- // Regression test for https://github.com/posit-dev/positron/issues/4197
- const app = this.app as Application;
- this.timeout(120000);
-
- const script = `Data_Frame <- mtcars`;
- await app.workbench.positronConsole.executeCode('R', script, '>');
- await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus');
-
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('Data_Frame');
- await app.code.driver.getLocator('.label-name:has-text("Data: Data_Frame")').innerText();
- }).toPass();
-
- // Now move focus out of the the data explorer pane
- await app.workbench.editors.newUntitledFile();
- await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus');
- await app.workbench.positronVariables.doubleClickVariableRow('Data_Frame');
-
- await expect(async () => {
- await app.code.driver.getLocator('.label-name:has-text("Data: Data_Frame")').innerText();
- }).toPass();
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- await app.workbench.quickaccess.runCommand('workbench.panel.positronVariables.focus');
-
- });
- });
-});
diff --git a/test/smoke/src/areas/positron/dataexplorer/headlessDataExplorer.test.ts b/test/smoke/src/areas/positron/dataexplorer/headlessDataExplorer.test.ts
deleted file mode 100644
index 03c086f9364..00000000000
--- a/test/smoke/src/areas/positron/dataexplorer/headlessDataExplorer.test.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures } from '../../../../../automation';
-import { join } from 'path';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-let logger;
-
-const LAST_CELL_CONTENTS = '2013-09-30 08:00:00';
-
-describe('Headless Data Explorer - Large Data Frame #web', () => {
- logger = setupAndStartApp();
-
- async function testBody(app: Application, fileName: string) {
-
- await app.workbench.positronQuickaccess.openDataFile(join(app.workspacePathOrFolder, 'data-files', 'flights', fileName));
-
- logger.log('Opening data grid');
- await expect(async () => {
- expect(await app.code.driver.getLocator(`.label-name:has-text("Data: ${fileName}")`).innerText() === `Data: ${fileName}`);
- }).toPass();
-
- await app.workbench.positronSideBar.closeSecondarySideBar();
-
- await expect(async () => {
- // Validate full grid by checking bottom right corner data
- await app.workbench.positronDataExplorer.clickLowerRightCorner();
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
- const lastRow = tableData.at(-1);
- const lastHour = lastRow!['time_hour'];
- expect(lastHour).toBe(LAST_CELL_CONTENTS);
- }).toPass();
- }
-
-
- describe('Headless Data Explorer (Large Data Frame)', () => {
-
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
- await this.app.workbench.positronLayouts.enterLayout('stacked');
-
- });
-
- afterEach(async function () {
- const app = this.app as Application;
- await app.workbench.positronDataExplorer.closeDataExplorer();
-
- });
-
- it('Verifies headless data explorer functionality with large parquet file [C938893]', async function () {
- await testBody(this.app as Application, 'flights.parquet');
- });
-
-
- it('Verifies headless data explorer functionality with large csv file [C938894]', async function () {
- await testBody(this.app as Application, 'flights.csv');
- });
- });
-
-});
diff --git a/test/smoke/src/areas/positron/dataexplorer/largeDataFrame.test.ts b/test/smoke/src/areas/positron/dataexplorer/largeDataFrame.test.ts
deleted file mode 100644
index 3f7103d0fc6..00000000000
--- a/test/smoke/src/areas/positron/dataexplorer/largeDataFrame.test.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { join } from 'path';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-let logger;
-
-const LAST_CELL_CONTENTS = '2013-09-30 08:00:00';
-const FILTER_PARAMS = ['distance', 'is equal to', '2586'];
-const POST_FILTER_DATA_SUMMARY = 'Showing 8,204 rows (2.44% of 336,776 total)  19 columns';
-
-describe('Data Explorer - Large Data Frame #pr #web #win', () => {
- logger = setupAndStartApp();
-
- describe('Python Data Explorer (Large Data Frame)', () => {
-
- before(async function () {
-
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
- await this.app.workbench.positronLayouts.enterLayout('stacked');
-
- });
-
- after(async function () {
-
- const app = this.app as Application;
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
-
- });
-
- it('Python - Verifies data explorer functionality with large data frame [C557555]', async function () {
- this.timeout(1200000);
- const app = this.app as Application;
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'nyc-flights-data-py', 'flights-data-frame.py'));
- await app.workbench.quickaccess.runCommand('python.execInConsole');
-
- logger.log('Opening data grid');
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('df');
- expect(await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText() === 'Data: df');
- }).toPass();
-
- await app.workbench.positronSideBar.closeSecondarySideBar();
-
- await expect(async () => {
- // Validate full grid by checking bottom right corner data
- await app.workbench.positronDataExplorer.clickLowerRightCorner();
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
- const lastRow = tableData.at(-1);
- const lastHour = lastRow!['time_hour'];
- expect(lastHour).toBe(LAST_CELL_CONTENTS);
- }).toPass();
-
- await expect(async () => {
- // Filter data set
- await app.workbench.positronDataExplorer.clickUpperLeftCorner();
- await app.workbench.positronDataExplorer.addFilter(...FILTER_PARAMS as [string, string, string]);
-
- const statusBar = await app.workbench.positronDataExplorer.getDataExplorerStatusBar();
- expect(statusBar.textContent).toBe(POST_FILTER_DATA_SUMMARY);
- }).toPass();
-
- });
- });
-
- describe('R Data Explorer (Large Data Frame) #pr #web', () => {
-
- before(async function () {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- await this.app.workbench.positronLayouts.enterLayout('stacked');
-
- });
-
- after(async function () {
-
- const app = this.app as Application;
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- });
-
- it('R - Verifies data explorer functionality with large data frame [C557554]', async function () {
- this.timeout(1200000);
- const app = this.app as Application;
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'nyc-flights-data-r', 'flights-data-frame.r'));
- await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
-
- logger.log('Opening data grid');
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('df2');
- expect(await app.code.driver.getLocator('.label-name:has-text("Data: df2")').innerText() === 'Data: df2');
- }).toPass();
-
- await app.workbench.positronSideBar.closeSecondarySideBar();
-
- await expect(async () => {
- // Validate full grid by checking bottom right corner data
- await app.workbench.positronDataExplorer.clickLowerRightCorner();
- const tableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
- const lastRow = tableData.at(-1);
- const lastHour = lastRow!['time_hour'];
- expect(lastHour).toBe(LAST_CELL_CONTENTS);
- }).toPass();
-
- await expect(async () => {
- // Filter data set
- await app.workbench.positronDataExplorer.clickUpperLeftCorner();
- await app.workbench.positronDataExplorer.addFilter(...FILTER_PARAMS as [string, string, string]);
- const statusBar = await app.workbench.positronDataExplorer.getDataExplorerStatusBar();
- expect(statusBar.textContent).toBe(POST_FILTER_DATA_SUMMARY);
- }).toPass();
-
- });
- });
-
-
-});
diff --git a/test/smoke/src/areas/positron/dataexplorer/sparklinesTrend.test.ts b/test/smoke/src/areas/positron/dataexplorer/sparklinesTrend.test.ts
deleted file mode 100644
index eab6ec1692a..00000000000
--- a/test/smoke/src/areas/positron/dataexplorer/sparklinesTrend.test.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Data Explorer #web #win', () => {
- setupAndStartApp();
-
- describe('Sparklines', () => {
- let app: Application;
-
- beforeEach(async function () {
- app = this.app;
- await app.workbench.positronLayouts.enterLayout('stacked');
- });
-
- afterEach(async () => {
- await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
- });
-
- it('Python Pandas - Verifies downward trending graph [C830552]', async () => {
- await PositronPythonFixtures.SetupFixtures(app);
-
- await app.workbench.positronConsole.executeCode('Python', pythonScript, '>>>');
- await openDataExplorerColumnProfile(app, 'pythonData');
- await verifyGraphBarHeights(app);
- });
-
-
- it('R - Verifies downward trending graph [C830553]', async () => {
- await PositronRFixtures.SetupFixtures(app);
-
- await app.workbench.positronConsole.executeCode('R', rScript, '>');
- await openDataExplorerColumnProfile(app, 'rData');
- await verifyGraphBarHeights(app);
- });
- });
-
- async function openDataExplorerColumnProfile(app: Application, variableName: string) {
-
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow(variableName);
- await app.code.driver.getLocator(`.label-name:has-text("Data: ${variableName}")`).innerText();
- }).toPass();
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleSidebarVisibility');
- await app.workbench.positronSideBar.closeSecondarySideBar();
- await app.workbench.positronDataExplorer.getDataExplorerTableData();
- await app.workbench.positronDataExplorer.expandColumnProfile(0);
- }
-
- async function verifyGraphBarHeights(app: Application) {
- // Get all graph graph bars/rectangles
- await expect(async () => {
- const rects = app.code.driver.getLocator('rect.count');
-
- // Iterate over each rect and verify the height
- const expectedHeights = ['50', '40', '30', '20', '10'];
- for (let i = 0; i < expectedHeights.length; i++) {
- const height = await rects.nth(i).getAttribute('height');
- expect(height).toBe(expectedHeights[i]);
- }
- }).toPass({ timeout: 10000 });
- }
-});
-
-
-const rScript = `library(dplyr)
-
-rData <- tibble(
-category = c("A", "A", "A", "A", "B", "B", "B", "C", "C", "D", "E", "A", "B", "C", "D"),
-values = c(1, 2, 3, 4, 5, 9, 10, 11, 13, 25, 7, 15, 20, 5, 6)
-)`;
-
-
-const pythonScript = `import pandas as pd
-import matplotlib.pyplot as plt
-
-pythonData = pd.DataFrame({
-'category': ['A', 'A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'D', 'E', 'A', 'B', 'C', 'D'],
-'values': [1, 2, 3, 4, 5, 9, 10, 11, 13, 25, 7, 15, 20, 5, 6]
-})`;
-
diff --git a/test/smoke/src/areas/positron/dataexplorer/xlsxDataFrame.test.ts b/test/smoke/src/areas/positron/dataexplorer/xlsxDataFrame.test.ts
deleted file mode 100644
index 7411612beb2..00000000000
--- a/test/smoke/src/areas/positron/dataexplorer/xlsxDataFrame.test.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { join } from 'path';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-let logger;
-
-describe('Data Explorer - XLSX #web #win', () => {
- logger = setupAndStartApp();
-
- describe('Python Data Explorer (XLSX file)', () => {
-
- before(async function () {
-
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
- });
-
- after(async function () {
-
- const app = this.app as Application;
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- await app.workbench.positronVariables.toggleVariablesView();
-
- });
-
- it('Python - Verifies data explorer functionality with XLSX input [C632940]', async function () {
-
- const app = this.app as Application;
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'read-xlsx-py', 'supermarket-sales.py'));
- await app.workbench.quickaccess.runCommand('python.execInConsole');
-
- logger.log('Opening data grid');
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('df');
- await app.code.driver.getLocator('.label-name:has-text("Data: df")').innerText();
- }).toPass();
-
- await app.workbench.positronSideBar.closeSecondarySideBar();
-
- await app.workbench.positronDataExplorer.selectColumnMenuItem(1, 'Sort Descending');
-
- const visibleTableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- const firstRow = visibleTableData.at(0);
- expect(firstRow!['Invoice ID']).toBe('898-04-2717');
-
- });
- });
-
- describe('R Data Explorer (XLSX file)', () => {
-
- before(async function () {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- });
-
- after(async function () {
-
- const app = this.app as Application;
-
- await app.workbench.positronDataExplorer.closeDataExplorer();
- await app.workbench.positronVariables.toggleVariablesView();
-
- });
-
- it('R - Verifies data explorer functionality with XLSX input [C632941]', async function () {
-
- const app = this.app as Application;
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'read-xlsx-r', 'supermarket-sales.r'));
- await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
-
- logger.log('Opening data grid');
- await expect(async () => {
- await app.workbench.positronVariables.doubleClickVariableRow('df2');
- await app.code.driver.getLocator('.label-name:has-text("Data: df2")').innerText();
- }).toPass();
-
- await app.workbench.positronSideBar.closeSecondarySideBar();
-
- await app.workbench.positronDataExplorer.selectColumnMenuItem(1, 'Sort Descending');
-
- const visibleTableData = await app.workbench.positronDataExplorer.getDataExplorerTableData();
-
- const firstRow = visibleTableData.at(0);
- expect(firstRow!['Invoice ID']).toBe('898-04-2717');
-
- });
- });
-
-});
diff --git a/test/smoke/src/areas/positron/editor/fast-execution.test.ts b/test/smoke/src/areas/positron/editor/fast-execution.test.ts
index 296c1fa5aa0..dea0951cc81 100644
--- a/test/smoke/src/areas/positron/editor/fast-execution.test.ts
+++ b/test/smoke/src/areas/positron/editor/fast-execution.test.ts
@@ -4,63 +4,51 @@
*--------------------------------------------------------------------------------------------*/
import { join } from 'path';
-import { Application, PositronRFixtures } from '../../../../../automation';
-import { expect } from '@playwright/test';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
-describe('Editor Pane: R #web', () => {
- setupAndStartApp();
- const FILENAME = 'fast-execution.r';
-
- describe('R Fast Execution', () => {
-
- beforeEach(async function () {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- });
-
- it('Verify fast execution is not out of order [C712539]', async function () {
- const app = this.app as Application;
-
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'fast-statement-execution', FILENAME));
-
- let previousTop = -1;
-
- // Note that this outer loop iterates 10 times. This is because the length of the
- // file fast-execution.r is 10 lines. We want to be sure to send a Control+Enter
- // for every line of the file
- for (let i = 0; i < 10; i++) {
- let currentTop = await app.workbench.positronEditor.getCurrentLineTop();
- let retries = 20;
-
- // Note that top is a measurement of the distance from the top of the editor
- // to the top of the current line. By monitoring the top value, we can determine
- // if the editor is advancing to the next line. Without this check, the test
- // would send Control+Enter many times to the first line of the file and not
- // perform the desired test.
- while (currentTop === previousTop && retries > 0) {
- currentTop = await app.workbench.positronEditor.getCurrentLineTop();
- retries--;
- }
-
- previousTop = currentTop;
+test.use({
+ suiteId: __filename
+});
- await app.code.driver.getKeyboard().press('Control+Enter');
+const FILENAME = 'fast-execution.r';
+
+test.describe('R Fast Execution', { tag: ['@web'] }, () => {
+ test('Verify fast execution is not out of order [C712539]', async function ({ app, r }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'fast-statement-execution', FILENAME));
+
+ let previousTop = -1;
+
+ // Note that this outer loop iterates 10 times. This is because the length of the
+ // file fast-execution.r is 10 lines. We want to be sure to send a Control+Enter
+ // for every line of the file
+ for (let i = 0; i < 10; i++) {
+ let currentTop = await app.workbench.positronEditor.getCurrentLineTop();
+ let retries = 20;
+
+ // Note that top is a measurement of the distance from the top of the editor
+ // to the top of the current line. By monitoring the top value, we can determine
+ // if the editor is advancing to the next line. Without this check, the test
+ // would send Control+Enter many times to the first line of the file and not
+ // perform the desired test.
+ while (currentTop === previousTop && retries > 0) {
+ currentTop = await app.workbench.positronEditor.getCurrentLineTop();
+ retries--;
}
- await app.workbench.positronVariables.waitForVariableRow('c');
+ previousTop = currentTop;
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ await app.code.driver.getKeyboard().press('Control+Enter');
+ }
- const variablesMap = await app.workbench.positronVariables.getFlatVariables();
+ await app.workbench.positronVariables.waitForVariableRow('c');
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ const variablesMap = await app.workbench.positronVariables.getFlatVariables();
- expect(variablesMap.get('x')).toStrictEqual({ value: '1', type: 'dbl' });
- expect(variablesMap.get('y')).toStrictEqual({ value: '1', type: 'dbl' });
- expect(variablesMap.get('z')).toStrictEqual({ value: '1', type: 'dbl' });
- expect(variablesMap.get('a')).toStrictEqual({ value: '1', type: 'dbl' });
- expect(variablesMap.get('b')).toStrictEqual({ value: '1', type: 'dbl' });
- expect(variablesMap.get('c')).toStrictEqual({ value: '1', type: 'dbl' });
- });
+ expect(variablesMap.get('x')).toStrictEqual({ value: '1', type: 'dbl' });
+ expect(variablesMap.get('y')).toStrictEqual({ value: '1', type: 'dbl' });
+ expect(variablesMap.get('z')).toStrictEqual({ value: '1', type: 'dbl' });
+ expect(variablesMap.get('a')).toStrictEqual({ value: '1', type: 'dbl' });
+ expect(variablesMap.get('b')).toStrictEqual({ value: '1', type: 'dbl' });
+ expect(variablesMap.get('c')).toStrictEqual({ value: '1', type: 'dbl' });
});
});
diff --git a/test/smoke/src/areas/positron/example.test.ts b/test/smoke/src/areas/positron/example.test.ts
deleted file mode 100644
index e0b8303d9e3..00000000000
--- a/test/smoke/src/areas/positron/example.test.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-// Note - these paths will need to change for your specific test location
-import { Application, PositronPythonFixtures } from '../../../../automation';
-import { setupAndStartApp } from '../../test-runner/test-hooks';
-
-describe('Major Test Area', () => {
- // Needed at parent `describe` block to setup shared before/after hooks
- // It does return the logger (in case you want/need to log in test)
- setupAndStartApp();
-
- describe('Minor Test area', () => {
-
- before(async function () {
- // Executes once before executing all tests.
- // Change to 'beforeEach' if it needs to run before each individual test.
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- it('Sample Test Case A [TESTRAIL_ID]', async function () {
- const app = this.app as Application; //Get handle to application
- await app.workbench.positronConsole.barPowerButton.waitforVisible();
- this.code.logger.log("Waiting for Power button.");
- });
-
- it('Sample Test Case B [TESTRAIL_ID]', async function () {
- const app = this.app as Application; //Get handle to application
- await app.workbench.positronConsole.barRestartButton.waitforVisible();
- this.code.logger.log("Waiting for Power button.");
- });
-
- });
-
-});
-
diff --git a/test/smoke/src/areas/positron/help/f1.test.ts b/test/smoke/src/areas/positron/help/f1.test.ts
index b820065e2c0..c09496cea6b 100644
--- a/test/smoke/src/areas/positron/help/f1.test.ts
+++ b/test/smoke/src/areas/positron/help/f1.test.ts
@@ -3,73 +3,46 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
import { join } from 'path';
+import { test, expect } from '../_test.setup';
-
-describe('F1 Help #web #win', () => {
- setupAndStartApp();
-
- describe('R F1 Help', () => {
-
- before(async function () {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- it('R - Verifies basic F1 help functionality [C1018854]', async function () {
-
- const app = this.app as Application;
-
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'nyc-flights-data-r', 'flights-data-frame.r'));
- await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
-
- await app.workbench.positronConsole.pasteCodeToConsole('colnames(df2)');
-
- await app.workbench.positronConsole.doubleClickConsoleText('colnames');
-
- await app.workbench.positronConsole.sendKeyboardKey('F1');
-
- await expect(async () => {
- const helpFrame = await app.workbench.positronHelp.getHelpFrame(0);
- await expect(helpFrame.locator('body')).toContainText('Row and Column Names', { timeout: 30000 });
- }).toPass({ timeout: 30000 });
-
- });
- });
+test.use({
+ suiteId: __filename
});
-describe('F1 Help #web #win #pr', () => {
- setupAndStartApp();
- describe('Python F1 Help', () => {
+test.describe('F1 Help #web #win', {
+ tag: ['@web', '@win']
+}, () => {
- before(async function () {
+ test('R - Verifies basic F1 help functionality [C1018854]', async function ({ app, r }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'nyc-flights-data-r', 'flights-data-frame.r'));
+ await app.workbench.quickaccess.runCommand('r.sourceCurrentFile');
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
+ await app.workbench.positronConsole.pasteCodeToConsole('colnames(df2)');
+ await app.workbench.positronConsole.doubleClickConsoleText('colnames');
+ await app.workbench.positronConsole.sendKeyboardKey('F1');
- it('Python - Verifies basic F1 help functionality [C1018854]', async function () {
+ await expect(async () => {
+ const helpFrame = await app.workbench.positronHelp.getHelpFrame(0);
+ await expect(helpFrame.locator('body')).toContainText('Row and Column Names', { timeout: 30000 });
+ }).toPass({ timeout: 30000 });
- const app = this.app as Application;
-
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'nyc-flights-data-py', 'flights-data-frame.py'));
- await app.workbench.quickaccess.runCommand('python.execInConsole');
+ });
- await app.workbench.positronConsole.pasteCodeToConsole('list(df.columns)');
- await app.workbench.positronConsole.doubleClickConsoleText('list');
+ test('Python - Verifies basic F1 help functionality [C1018854]', async function ({ app, python }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'nyc-flights-data-py', 'flights-data-frame.py'));
+ await app.workbench.quickaccess.runCommand('python.execInConsole');
- await app.workbench.positronConsole.sendKeyboardKey('F1');
+ await app.workbench.positronConsole.pasteCodeToConsole('list(df.columns)');
+ await app.workbench.positronConsole.doubleClickConsoleText('list');
+ await app.workbench.positronConsole.sendKeyboardKey('F1');
- await expect(async () => {
- const helpFrame = await app.workbench.positronHelp.getHelpFrame(0);
- await expect(helpFrame.locator('p').first()).toContainText('Built-in mutable sequence.', { timeout: 30000 });
- }).toPass({ timeout: 30000 });
+ await expect(async () => {
+ const helpFrame = await app.workbench.positronHelp.getHelpFrame(0);
+ await expect(helpFrame.locator('p').first()).toContainText('Built-in mutable sequence.', { timeout: 30000 });
+ }).toPass({ timeout: 30000 });
- });
});
});
diff --git a/test/smoke/src/areas/positron/help/help.test.ts b/test/smoke/src/areas/positron/help/help.test.ts
index fddf0e17160..ff2bb98ec7f 100644
--- a/test/smoke/src/areas/positron/help/help.test.ts
+++ b/test/smoke/src/areas/positron/help/help.test.ts
@@ -3,126 +3,98 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
+import { test, expect } from '../_test.setup';
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-let logger;
-
-describe('Help', () => {
- logger = setupAndStartApp();
-
- describe('Python Help', () => {
-
- before(async function () {
-
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
- });
+test.use({
+ suiteId: __filename
+});
- it('Python - Verifies basic help functionality [C633814]', async function () {
+test.describe('Help', () => {
- const app = this.app as Application;
- await app.workbench.positronConsole.executeCode('Python', `?load`, '>>>');
+ test('Python - Verifies basic help functionality [C633814]', async function ({ app, python }) {
+ await app.workbench.positronConsole.executeCode('Python', `?load`, '>>>');
- await expect(async () => {
- const helpFrame = await app.workbench.positronHelp.getHelpFrame(0);
- await expect(helpFrame.locator('body')).toContainText('Load code into the current frontend.');
- }).toPass();
+ await expect(async () => {
+ const helpFrame = await app.workbench.positronHelp.getHelpFrame(0);
+ await expect(helpFrame.locator('body')).toContainText('Load code into the current frontend.');
+ }).toPass();
- });
});
- describe('R Help', () => {
-
- before(async function () {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- });
-
- it('R - Verifies basic help functionality [C633813]', async function () {
-
- const app = this.app as Application;
- await app.workbench.positronConsole.executeCode('R', `?load()`, '>');
+ test('R - Verifies basic help functionality [C633813]', async function ({ app, r }) {
+ await app.workbench.positronConsole.executeCode('R', `?load()`, '>');
- await expect(async () => {
- const helpFrame = await app.workbench.positronHelp.getHelpFrame(1);
- await expect(helpFrame.locator('body')).toContainText('Reload Saved Datasets');
- }).toPass();
+ await expect(async () => {
+ const helpFrame = await app.workbench.positronHelp.getHelpFrame(1);
+ await expect(helpFrame.locator('body')).toContainText('Reload Saved Datasets');
+ }).toPass();
- });
});
- describe('Collapse behavior', () => {
-
- it('Verifies help panel can be opened when empty and also can be resized smaller and remember resize height [C640934]', async function () {
-
- const app = this.app as Application;
- const positronHelp = app.workbench.positronHelp;
- const helpContainerLocator = positronHelp.getHelpContainer();
- const helpPanelHeaderLocator = positronHelp.getHelpHeader();
- const getHelpHeight = async () => (await helpContainerLocator.boundingBox())?.height ?? -1;
-
- // How close should our heights be? It's not totally clear why this isn't always
- // exact, but it's likely due to rounding errors or other factors. We'll allow
- // a small margin of error.
- const sizePrecision = 5;
-
- // Enable reduced motion so we don't have to wait for animations of expanding
- // and collapsing the panel.
- await app.workbench.settingsEditor.addUserSetting('workbench.reduceMotion', '"on"');
-
- // Enter layout with help pane docked in session panel
- await app.workbench.positronLayouts.enterLayout('dockedHelp');
-
- // Help panel starts collapsed thanks to the above command
- await expect(helpContainerLocator).not.toBeVisible();
-
- // Clicking the header opens it
- await helpPanelHeaderLocator.click();
- await expect(helpContainerLocator).toBeVisible();
-
- // Make sure that an empty help panel actually expands to a visible size.
- const helpPanelHeight = await getHelpHeight();
- expect(helpPanelHeight).toBeGreaterThan(100);
-
- // Now resize the help panel smaller than the pop-open size and make sure that
- // when we collapse and reopen it doesn't pop back to the full size again.
-
- // We'll make it roughly two thirds the size of the original height
- const resize_delta = helpPanelHeight / 3;
- const expectedHeightAfterResize = helpPanelHeight - resize_delta;
- await positronHelp.resizeHelpPanel({ y: resize_delta });
-
- // Verify that the height has changed by the expected amount
- const helpPanelHeightAfter = await getHelpHeight();
- expect(expectedHeightAfterResize - helpPanelHeightAfter)
- .toBeLessThan(sizePrecision);
-
- // Now collapse the panel again
- await helpPanelHeaderLocator.click();
- await expect(helpContainerLocator).not.toBeVisible();
-
- // Reopen the panel
- await helpPanelHeaderLocator.click();
-
- if (helpPanelHeightAfter < 100) {
- // When the panel is small enough, it will pop back to the full size.
- // This can happen if the window used for testing is too small.
- // In this case we want to end the test early because the behavior wont be as
- // expected.
- // TODO: Make sure window is a set size at start of test.
- logger.log('Window too small to test resize memory. Skipping end of help panel collapse test.');
- return;
- }
- // Make sure that the panel is smaller than it was before after opening up.
- // Should be roughly the same size it was before we collapsed it. Allow for
- // small deviations due to rounding errors etc..
- const helpPanelHeightAfterReopen = await getHelpHeight();
- expect(Math.abs(helpPanelHeightAfterReopen - helpPanelHeightAfter))
- .toBeLessThan(sizePrecision);
- });
+ test('Verifies help panel can be opened when empty and also can be resized smaller and remember resize height [C640934]', async function ({ app, logger }) {
+
+ const positronHelp = app.workbench.positronHelp;
+ const helpContainerLocator = positronHelp.getHelpContainer();
+ const helpPanelHeaderLocator = positronHelp.getHelpHeader();
+ const getHelpHeight = async () => (await helpContainerLocator.boundingBox())?.height ?? -1;
+
+ // How close should our heights be? It's not totally clear why this isn't always
+ // exact, but it's likely due to rounding errors or other factors. We'll allow
+ // a small margin of error.
+ const sizePrecision = 5;
+
+ // Enable reduced motion so we don't have to wait for animations of expanding
+ // and collapsing the panel.
+ await app.workbench.settingsEditor.addUserSetting('workbench.reduceMotion', '"on"');
+
+ // Enter layout with help pane docked in session panel
+ await app.workbench.positronLayouts.enterLayout('dockedHelp');
+
+ // Help panel starts collapsed thanks to the above command
+ await expect(helpContainerLocator).not.toBeVisible();
+
+ // Clicking the header opens it
+ await helpPanelHeaderLocator.click();
+ await expect(helpContainerLocator).toBeVisible();
+
+ // Make sure that an empty help panel actually expands to a visible size.
+ const helpPanelHeight = await getHelpHeight();
+ expect(helpPanelHeight).toBeGreaterThan(100);
+
+ // Now resize the help panel smaller than the pop-open size and make sure that
+ // when we collapse and reopen it doesn't pop back to the full size again.
+
+ // We'll make it roughly two thirds the size of the original height
+ const resize_delta = helpPanelHeight / 3;
+ const expectedHeightAfterResize = helpPanelHeight - resize_delta;
+ await positronHelp.resizeHelpPanel({ y: resize_delta });
+
+ // Verify that the height has changed by the expected amount
+ const helpPanelHeightAfter = await getHelpHeight();
+ expect(expectedHeightAfterResize - helpPanelHeightAfter)
+ .toBeLessThan(sizePrecision);
+
+ // Now collapse the panel again
+ await helpPanelHeaderLocator.click();
+ await expect(helpContainerLocator).not.toBeVisible();
+
+ // Reopen the panel
+ await helpPanelHeaderLocator.click();
+
+ if (helpPanelHeightAfter < 100) {
+ // When the panel is small enough, it will pop back to the full size.
+ // This can happen if the window used for testing is too small.
+ // In this case we want to end the test early because the behavior wont be as
+ // expected.
+ // TODO: Make sure window is a set size at start of test.
+ logger.log('Window too small to test resize memory. Skipping end of help panel collapse test.');
+ return;
+ }
+ // Make sure that the panel is smaller than it was before after opening up.
+ // Should be roughly the same size it was before we collapsed it. Allow for
+ // small deviations due to rounding errors etc..
+ const helpPanelHeightAfterReopen = await getHelpHeight();
+ expect(Math.abs(helpPanelHeightAfterReopen - helpPanelHeightAfter))
+ .toBeLessThan(sizePrecision);
});
});
diff --git a/test/smoke/src/areas/positron/layouts/layouts.test.ts b/test/smoke/src/areas/positron/layouts/layouts.test.ts
index 5f0c1d9690c..40c74c2bb40 100644
--- a/test/smoke/src/areas/positron/layouts/layouts.test.ts
+++ b/test/smoke/src/areas/positron/layouts/layouts.test.ts
@@ -3,19 +3,17 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
+import { test, expect } from '../_test.setup';
-import { expect } from '@playwright/test';
-import { Application } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Layouts #web', () => {
- setupAndStartApp();
+test.use({
+ suiteId: __filename
+});
- describe('Stacked Layout', () => {
+test.describe('Layouts', { tag: ['@web'] }, () => {
- it('Verify stacked layout puts stuff in appropriate places [C656294]', async function () {
+ test.describe('Stacked Layout', () => {
- const app = this.app as Application;
+ test('Verify stacked layout puts stuff in appropriate places [C656294]', async function ({ app }) {
const layouts = app.workbench.positronLayouts;
await app.code.driver.setViewportSize({ width: 1400, height: 1000 });
@@ -58,11 +56,10 @@ describe('Layouts #web', () => {
});
});
- describe('Side-by-side Layout', () => {
+ test.describe('Side-by-side Layout', () => {
- it('Verify Side-by-side layout puts stuff in appropriate places [C656295]', async function () {
+ test('Verify Side-by-side layout puts stuff in appropriate places [C656295]', async function ({ app }) {
- const app = this.app as Application;
const layouts = app.workbench.positronLayouts;
// Enter layout with help pane docked in session panel
@@ -101,11 +98,10 @@ describe('Layouts #web', () => {
});
});
- describe('Notebook Layout', () => {
+ test.describe('Notebook Layout', () => {
- it('Verify notebook layout puts stuff in appropriate places [C656296]', async function () {
+ test('Verify notebook layout puts stuff in appropriate places [C656296]', async function ({ app }) {
- const app = this.app as Application;
const layouts = app.workbench.positronLayouts;
// Enter layout with help pane docked in session panel
diff --git a/test/smoke/src/areas/positron/new-project-wizard/new-project-python.test.ts b/test/smoke/src/areas/positron/new-project-wizard/new-project-python.test.ts
new file mode 100644
index 00000000000..a82a21ed9cd
--- /dev/null
+++ b/test/smoke/src/areas/positron/new-project-wizard/new-project-python.test.ts
@@ -0,0 +1,189 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { PositronPythonFixtures, ProjectType, ProjectWizardNavigateAction } from '../../../../../automation';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.beforeEach(async function ({ app }) {
+ await app.workbench.positronConsole.waitForReadyOrNoInterpreter();
+});
+
+test.describe('Python - New Project Wizard', () => {
+ const defaultProjectName = 'my-python-project';
+
+ test('Create a new Conda environment [C628628]', async function ({ app, page }) {
+ // This test relies on Conda already being installed on the machine
+ test.slow();
+ const projSuffix = addRandomNumSuffix('_condaInstalled');
+ const pw = app.workbench.positronNewProjectWizard;
+ await pw.startNewProject(ProjectType.PYTHON_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ // Select 'Conda' as the environment provider
+ await pw.pythonConfigurationStep.selectEnvProvider('Conda');
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 });
+ // Check that the `.conda` folder gets created in the project
+ await expect(async () => {
+ const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
+ expect(projectFiles).toContain('.conda');
+ }).toPass({ timeout: 50000 });
+ // The console should initialize without any prompts to install ipykernel
+ await expect(app.workbench.positronConsole.activeConsole.getByText('>>>')).toBeVisible({ timeout: 45000 });
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ });
+
+ test('Create a new Venv environment [C627912]', { tag: ['@pr'] }, async function ({ app, page }) {
+ // This is the default behavior for a new Python Project in the Project Wizard
+ const projSuffix = addRandomNumSuffix('_new_venv');
+ const pw = app.workbench.positronNewProjectWizard;
+ await pw.startNewProject(ProjectType.PYTHON_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 });
+ await expect(app.workbench.positronConsole.activeConsole.getByText('>>>')).toBeVisible({ timeout: 45000 });
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ });
+
+ test.skip('With ipykernel already installed [C609619]', {
+ annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5286' }]
+ }, async function ({ app, page, python }) {
+ const projSuffix = addRandomNumSuffix('_ipykernelInstalled');
+ const pw = app.workbench.positronNewProjectWizard;
+ const pythonFixtures = new PositronPythonFixtures(app);
+ // Start the Python interpreter and ensure ipykernel is installed
+ await pythonFixtures.startAndGetPythonInterpreter(true);
+
+ const interpreterInfo =
+ await app.workbench.positronInterpreterDropdown.getSelectedInterpreterInfo();
+ expect(interpreterInfo?.path).toBeDefined();
+ await app.workbench.positronInterpreterDropdown.closeInterpreterDropdown();
+ // Create a new Python project and use the selected python interpreter
+ await pw.startNewProject(ProjectType.PYTHON_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.pythonConfigurationStep.existingEnvRadioButton.click();
+ // Select the interpreter that was started above. It's possible that this needs
+ // to be attempted a few times to ensure the interpreters are properly loaded.
+ await expect(
+ async () =>
+ await pw.pythonConfigurationStep.selectInterpreterByPath(
+ interpreterInfo!.path
+ )
+ ).toPass({
+ intervals: [1_000, 2_000, 10_000],
+ timeout: 50_000
+ });
+ await expect(pw.pythonConfigurationStep.interpreterFeedback).not.toBeVisible();
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 });
+ await expect(app.workbench.positronConsole.activeConsole.getByText('>>>')).toBeVisible({ timeout: 45000 });
+ });
+
+ test.skip('With ipykernel not already installed [C609617]', {
+ annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5286' }]
+ }, async function ({ app, page }) {
+ const projSuffix = addRandomNumSuffix('_noIpykernel');
+ const pw = app.workbench.positronNewProjectWizard;
+ const pythonFixtures = new PositronPythonFixtures(app);
+ // Start the Python interpreter and uninstall ipykernel
+ await pythonFixtures.startAndGetPythonInterpreter(true);
+
+ const interpreterInfo =
+ await app.workbench.positronInterpreterDropdown.getSelectedInterpreterInfo();
+ expect(interpreterInfo?.path).toBeDefined();
+ await app.workbench.positronInterpreterDropdown.closeInterpreterDropdown();
+ await app.workbench.positronConsole.typeToConsole('pip uninstall -y ipykernel');
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronConsole.waitForConsoleContents((contents) =>
+ contents.some((line) => line.includes('Successfully uninstalled ipykernel'))
+ );
+ // Create a new Python project and use the selected python interpreter
+ await pw.startNewProject(ProjectType.PYTHON_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ // Choose the existing environment which does not have ipykernel
+ await pw.pythonConfigurationStep.existingEnvRadioButton.click();
+ // Select the interpreter that was started above. It's possible that this needs
+ // to be attempted a few times to ensure the interpreters are properly loaded.
+ await expect(
+ async () =>
+ await pw.pythonConfigurationStep.selectInterpreterByPath(
+ interpreterInfo!.path
+ )
+ ).toPass({
+ intervals: [1_000, 2_000, 10_000],
+ timeout: 50_000
+ });
+ await expect(pw.pythonConfigurationStep.interpreterFeedback).toHaveText(
+ 'ipykernel will be installed for Python language support.',
+ { timeout: 10_000 }
+ );
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 });
+
+ // If ipykernel was successfully installed during the new project initialization,
+ // the console should be ready without any prompts to install ipykernel
+ await expect(app.workbench.positronConsole.activeConsole.getByText('>>>')).toBeVisible({ timeout: 45000 });
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ });
+
+ test('Default Python Project with git init [C674522]', { tag: ['@pr', '@win'] }, async function ({ app, page }) {
+ const projSuffix = addRandomNumSuffix('_gitInit');
+ const pw = app.workbench.positronNewProjectWizard;
+ await pw.startNewProject(ProjectType.PYTHON_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+
+ // Check the git init checkbox
+ await pw.projectNameLocationStep.gitInitCheckbox.waitFor();
+ await pw.projectNameLocationStep.gitInitCheckbox.setChecked(true);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+
+ // Open the new project in the current window and wait for the console to be ready
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 20000 });
+ await expect(app.workbench.positronConsole.activeConsole.getByText('>>>')).toBeVisible({ timeout: 45000 });
+
+ // Verify git-related files are present
+ await expect(async () => {
+ const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
+ expect(projectFiles).toContain('.gitignore');
+ expect(projectFiles).toContain('README.md');
+ // Ideally, we'd check for the .git folder, but it's not visible in the Explorer
+ // by default due to the default `files.exclude` setting in the workspace.
+ }).toPass({ timeout: 50000 });
+
+ // Git status should show that we're on the main branch
+ await app.workbench.terminal.createTerminal();
+ await app.workbench.terminal.runCommandInTerminal('git status');
+ await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(e => e.includes('On branch main')));
+ });
+});
+
+function addRandomNumSuffix(name: string): string {
+ return `${name}_${Math.floor(Math.random() * 1000000)}`;
+}
+
diff --git a/test/smoke/src/areas/positron/new-project-wizard/new-project-r-jupyter.test.ts b/test/smoke/src/areas/positron/new-project-wizard/new-project-r-jupyter.test.ts
new file mode 100644
index 00000000000..5d70e00471b
--- /dev/null
+++ b/test/smoke/src/areas/positron/new-project-wizard/new-project-r-jupyter.test.ts
@@ -0,0 +1,155 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { ProjectType, ProjectWizardNavigateAction } from '../../../../../automation';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.beforeEach(async function ({ app }) {
+ await app.workbench.positronConsole.waitForReadyOrNoInterpreter();
+});
+
+test.describe('R - New Project Wizard', () => {
+ test.describe.configure({ mode: 'serial' });
+
+ const defaultProjectName = 'my-r-project';
+
+ test('R - Project Defaults [C627913]', { tag: ['@pr', '@win'] }, async function ({ app }) {
+ const projSuffix = addRandomNumSuffix('_defaults');
+ const pw = app.workbench.positronNewProjectWizard;
+ await pw.startNewProject(ProjectType.R_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 });
+ // NOTE: For completeness, we probably want to await app.workbench.positronConsole.waitForReady('>', 10000);
+ // here, but it's timing out in CI, so it is not included for now.
+ });
+
+ test('R - Accept Renv install [C633084]', async function ({ app, r }) {
+ const projSuffix = addRandomNumSuffix('_installRenv');
+ const pw = app.workbench.positronNewProjectWizard;
+ // Create a new R project - select Renv and install
+ await pw.startNewProject(ProjectType.R_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ // Select the renv checkbox
+ await pw.rConfigurationStep.renvCheckbox.click();
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 });
+ // Interact with the modal to install renv
+ await app.workbench.positronPopups.installRenv();
+
+ // If this test is running on a machine that is using Renv for the first time, we
+ // may need to interact with the Console to allow the renv installation to complete
+ // An example: https://github.com/posit-dev/positron/pull/3881#issuecomment-2211123610.
+
+ // You should either manually interact with the Console to proceed with the Renv
+ // install or temporarily uncomment the code below to automate the interaction.
+ // await app.workbench.positronConsole.waitForConsoleContents((contents) =>
+ // contents.some((line) => line.includes('Do you want to proceed?'))
+ // );
+ // await app.workbench.positronConsole.typeToConsole('y');
+ // await app.workbench.positronConsole.sendEnterKey();
+
+ // Verify renv files are present
+ await expect(async () => {
+ const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
+ expect(projectFiles).toContain('renv');
+ expect(projectFiles).toContain('.Rprofile');
+ expect(projectFiles).toContain('renv.lock');
+ }).toPass({ timeout: 50000 });
+ // Verify that renv output in the console confirms no issues occurred
+ await app.workbench.positronConsole.waitForConsoleContents((contents) =>
+ contents.some((line) => line.includes('renv activated'))
+ );
+ });
+
+ test('R - Renv already installed [C656251]', async function ({ app }) {
+ // Renv will already be installed from the previous test - which is why tests are marked as "serial"
+ const projSuffix = addRandomNumSuffix('_renvAlreadyInstalled');
+ const pw = app.workbench.positronNewProjectWizard;
+ await pw.startNewProject(ProjectType.R_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ // Select the renv checkbox
+ await pw.rConfigurationStep.renvCheckbox.click();
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 });
+ // Verify renv files are present
+ await expect(async () => {
+ const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
+ expect(projectFiles).toContain('renv');
+ expect(projectFiles).toContain('.Rprofile');
+ expect(projectFiles).toContain('renv.lock');
+ }).toPass({ timeout: 50000 });
+ // Verify that renv output in the console confirms no issues occurred
+ await app.workbench.positronConsole.waitForConsoleContents((contents) =>
+ contents.some((line) => line.includes('renv activated'))
+ );
+ });
+
+ test('R - Cancel Renv install [C656252]', async function ({ app }) {
+ const projSuffix = addRandomNumSuffix('_cancelRenvInstall');
+ const pw = app.workbench.positronNewProjectWizard;
+ // Remove renv package so we are prompted to install it again
+ await app.workbench.positronConsole.executeCode('R', 'remove.packages("renv")', '>');
+ await app.workbench.positronConsole.waitForConsoleContents((contents) =>
+ contents.some((line) => line.includes(`Removing package`))
+ );
+ // Create a new R project - select Renv but opt out of installing
+ await pw.startNewProject(ProjectType.R_PROJECT);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ // Select the renv checkbox
+ await pw.rConfigurationStep.renvCheckbox.click();
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 });
+ // Interact with the modal to skip installing renv
+ await app.workbench.positronPopups.installRenv(false);
+ // Verify renv files are **not** present
+ await expect(async () => {
+ const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
+ expect(projectFiles).not.toContain('renv');
+ expect(projectFiles).not.toContain('.Rprofile');
+ expect(projectFiles).not.toContain('renv.lock');
+ }).toPass({ timeout: 50000 });
+ });
+
+});
+
+test.describe('Jupyter - New Project Wizard', () => {
+ const defaultProjectName = 'my-jupyter-notebook';
+
+ test('Jupyter Project Defaults [C629352]', { tag: ['@pr'] }, async function ({ app }) {
+ const projSuffix = addRandomNumSuffix('_defaults');
+ const pw = app.workbench.positronNewProjectWizard;
+ await pw.startNewProject(ProjectType.JUPYTER_NOTEBOOK);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.projectNameLocationStep.appendToProjectName(projSuffix);
+ await pw.navigate(ProjectWizardNavigateAction.NEXT);
+ await pw.navigate(ProjectWizardNavigateAction.CREATE);
+ await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
+ await app.code.driver.wait(10000);
+ await expect(app.code.driver.page.getByRole('button', { name: `Explorer Section: ${defaultProjectName + projSuffix}` })).toBeVisible({ timeout: 15000 });
+ // NOTE: For completeness, we probably want to await app.workbench.positronConsole.waitForReady('>>>', 10000);
+ // here, but it's timing out in CI, so it is not included for now.
+ });
+});
+
+function addRandomNumSuffix(name: string): string {
+ return `${name}_${Math.floor(Math.random() * 1000000)}`;
+}
diff --git a/test/smoke/src/areas/positron/new-project-wizard/new-project.test.ts b/test/smoke/src/areas/positron/new-project-wizard/new-project.test.ts
deleted file mode 100644
index e0769e4ad60..00000000000
--- a/test/smoke/src/areas/positron/new-project-wizard/new-project.test.ts
+++ /dev/null
@@ -1,346 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, ProjectType, ProjectWizardNavigateAction } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('New Project Wizard', () => {
- setupAndStartApp();
-
- describe('Python - New Project Wizard', () => {
- const defaultProjectName = 'my-python-project';
-
- describe('Python Project with new environment', () => {
- it('Create a new Venv environment [C627912] #pr', async function () {
- // This is the default behaviour for a new Python Project in the Project Wizard
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- await pw.startNewProject(ProjectType.PYTHON_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(defaultProjectName);
- await app.workbench.positronConsole.waitForReady('>>>', 10000);
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- });
- it('Create a new Conda environment [C628628]', async function () {
- // This test relies on Conda already being installed on the machine
- this.timeout(180000);
-
- const projSuffix = addRandomNumSuffix('_condaInstalled');
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- await pw.startNewProject(ProjectType.PYTHON_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.projectNameLocationStep.appendToProjectName(projSuffix);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- // Select 'Conda' as the environment provider
- await pw.pythonConfigurationStep.selectEnvProvider('Conda');
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(
- defaultProjectName + projSuffix
- );
- // Check that the `.conda` folder gets created in the project
- await expect(async () => {
- const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
- expect(projectFiles).toContain('.conda');
- }).toPass({ timeout: 50000 });
- // The console should initialize without any prompts to install ipykernel
- await app.workbench.positronConsole.waitForReady('>>>', 40000);
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- });
- });
-
- describe('Python Project with existing interpreter', () => {
- it('With ipykernel already installed [C609619]', async function () {
- const projSuffix = addRandomNumSuffix('_ipykernelInstalled');
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- const pythonFixtures = new PositronPythonFixtures(app);
- // Start the Python interpreter and ensure ipykernel is installed
- await pythonFixtures.startAndGetPythonInterpreter(true);
- // Ensure the console is ready with the selected interpreter
- await app.workbench.positronConsole.waitForReady('>>>', 10000);
- const interpreterInfo =
- await app.workbench.positronInterpreterDropdown.getSelectedInterpreterInfo();
- expect(interpreterInfo?.path).toBeDefined();
- await app.workbench.positronInterpreterDropdown.closeInterpreterDropdown();
- // Create a new Python project and use the selected python interpreter
- await pw.startNewProject(ProjectType.PYTHON_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.projectNameLocationStep.appendToProjectName(projSuffix);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.pythonConfigurationStep.existingEnvRadioButton.click();
- // Select the interpreter that was started above. It's possible that this needs
- // to be attempted a few times to ensure the interpreters are properly loaded.
- await expect(
- async () =>
- await pw.pythonConfigurationStep.selectInterpreterByPath(
- interpreterInfo!.path
- )
- ).toPass({
- intervals: [1_000, 2_000, 10_000],
- timeout: 50_000
- });
- await expect(pw.pythonConfigurationStep.interpreterFeedback).not.toBeVisible();
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(
- defaultProjectName + projSuffix
- );
- // The console should initialize without any prompts to install ipykernel
- await app.workbench.positronConsole.waitForReady('>>>', 10000);
- });
-
- it('With ipykernel not already installed [C609617]', async function () {
- const projSuffix = addRandomNumSuffix('_noIpykernel');
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- const pythonFixtures = new PositronPythonFixtures(app);
- // Start the Python interpreter and uninstall ipykernel
- await pythonFixtures.startAndGetPythonInterpreter(true);
- // Ensure the console is ready with the selected interpreter
- await app.workbench.positronConsole.waitForReady('>>>', 10000);
- const interpreterInfo =
- await app.workbench.positronInterpreterDropdown.getSelectedInterpreterInfo();
- expect(interpreterInfo?.path).toBeDefined();
- await app.workbench.positronInterpreterDropdown.closeInterpreterDropdown();
- await app.workbench.positronConsole.typeToConsole('pip uninstall -y ipykernel');
- await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronConsole.waitForConsoleContents((contents) =>
- contents.some((line) => line.includes('Successfully uninstalled ipykernel'))
- );
- // Create a new Python project and use the selected python interpreter
- await pw.startNewProject(ProjectType.PYTHON_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.projectNameLocationStep.appendToProjectName(projSuffix);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- // Choose the existing environment which does not have ipykernel
- await pw.pythonConfigurationStep.existingEnvRadioButton.click();
- // Select the interpreter that was started above. It's possible that this needs
- // to be attempted a few times to ensure the interpreters are properly loaded.
- await expect(
- async () =>
- await pw.pythonConfigurationStep.selectInterpreterByPath(
- interpreterInfo!.path
- )
- ).toPass({
- intervals: [1_000, 2_000, 10_000],
- timeout: 50_000
- });
- await expect(pw.pythonConfigurationStep.interpreterFeedback).toHaveText(
- 'ipykernel will be installed for Python language support.',
- { timeout: 10_000 }
- );
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(
- defaultProjectName + projSuffix
- );
- // If ipykernel was successfully installed during the new project initialization,
- // the console should be ready without any prompts to install ipykernel
- await app.workbench.positronConsole.waitForReady('>>>', 10000);
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- });
- });
-
- it('Default Python Project with git init [C674522] #pr #win', async function () {
- const projSuffix = addRandomNumSuffix('_gitInit');
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- await pw.startNewProject(ProjectType.PYTHON_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.projectNameLocationStep.appendToProjectName(projSuffix);
-
- // Check the git init checkbox
- await pw.projectNameLocationStep.gitInitCheckbox.waitFor();
- await pw.projectNameLocationStep.gitInitCheckbox.setChecked(true);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
-
- // Open the new project in the current window and wait for the console to be ready
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(
- defaultProjectName + projSuffix
- );
- await app.workbench.positronConsole.waitForReady('>>>', 10000);
-
- // Verify git-related files are present
- await expect(async () => {
- const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
- expect(projectFiles).toContain('.gitignore');
- expect(projectFiles).toContain('README.md');
- // Ideally, we'd check for the .git folder, but it's not visible in the Explorer
- // by default due to the default `files.exclude` setting in the workspace.
- }).toPass({ timeout: 50000 });
-
- // Git status should show that we're on the main branch
- await app.workbench.terminal.createTerminal();
- await app.workbench.terminal.runCommandInTerminal('git status');
- await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(e => e.includes('On branch main')));
- });
- });
-
- describe('R - New Project Wizard', () => {
- const defaultProjectName = 'my-r-project';
-
- it('R Project Defaults [C627913] #pr #win', async function () {
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- await pw.startNewProject(ProjectType.R_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(defaultProjectName);
- // NOTE: For completeness, we probably want to await app.workbench.positronConsole.waitForReady('>', 10000);
- // here, but it's timing out in CI, so it is not included for now.
- });
-
- describe('R Project with Renv Environment', () => {
- beforeEach(async function () {
- this.timeout(120000);
- const app = this.app as Application;
- await app.workbench.positronConsole.waitForReadyOrNoInterpreter();
- });
-
- it('Accept Renv install [C633084]', async function () {
- const projSuffix = addRandomNumSuffix('_installRenv');
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- // Create a new R project - select Renv and install
- await pw.startNewProject(ProjectType.R_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.projectNameLocationStep.appendToProjectName(projSuffix);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- // Select the renv checkbox
- await pw.rConfigurationStep.renvCheckbox.click();
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(
- defaultProjectName + projSuffix
- );
- // Interact with the modal to install renv
- await app.workbench.positronPopups.installRenv();
-
- // If this test is running on a machine that is using Renv for the first time, we
- // may need to interact with the Console to allow the renv installation to complete
- // An example: https://github.com/posit-dev/positron/pull/3881#issuecomment-2211123610.
-
- // You should either manually interact with the Console to proceed with the Renv
- // install or temporarily uncomment the code below to automate the interaction.
- // await app.workbench.positronConsole.waitForConsoleContents((contents) =>
- // contents.some((line) => line.includes('Do you want to proceed?'))
- // );
- // await app.workbench.positronConsole.typeToConsole('y');
- // await app.workbench.positronConsole.sendEnterKey();
-
- // Verify renv files are present
- await expect(async () => {
- const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
- expect(projectFiles).toContain('renv');
- expect(projectFiles).toContain('.Rprofile');
- expect(projectFiles).toContain('renv.lock');
- }).toPass({ timeout: 50000 });
- // Verify that renv output in the console confirms no issues occurred
- await app.workbench.positronConsole.waitForConsoleContents((contents) =>
- contents.some((line) => line.includes('renv activated'))
- );
- });
-
- it('Renv already installed [C656251]', async function () {
- // Renv will already be installed from the previous test
- const projSuffix = addRandomNumSuffix('_renvAlreadyInstalled');
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- await pw.startNewProject(ProjectType.R_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.projectNameLocationStep.appendToProjectName(projSuffix);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- // Select the renv checkbox
- await pw.rConfigurationStep.renvCheckbox.click();
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(
- defaultProjectName + projSuffix
- );
- // Verify renv files are present
- await expect(async () => {
- const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
- expect(projectFiles).toContain('renv');
- expect(projectFiles).toContain('.Rprofile');
- expect(projectFiles).toContain('renv.lock');
- }).toPass({ timeout: 50000 });
- // Verify that renv output in the console confirms no issues occurred
- await app.workbench.positronConsole.waitForConsoleContents((contents) =>
- contents.some((line) => line.includes('renv activated'))
- );
- });
-
- it('Cancel Renv install [C656252]', async function () {
- const projSuffix = addRandomNumSuffix('_cancelRenvInstall');
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- // Remove renv package so we are prompted to install it again
- await app.workbench.positronConsole.executeCode('R', 'remove.packages("renv")', '>');
- await app.workbench.positronConsole.waitForConsoleContents((contents) =>
- contents.some((line) => line.includes(`Removing package`))
- );
- // Create a new R project - select Renv but opt out of installing
- await pw.startNewProject(ProjectType.R_PROJECT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.projectNameLocationStep.appendToProjectName(projSuffix);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- // Select the renv checkbox
- await pw.rConfigurationStep.renvCheckbox.click();
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(
- defaultProjectName + projSuffix
- );
- // Interact with the modal to skip installing renv
- await app.workbench.positronPopups.installRenv(false);
- // Verify renv files are **not** present
- await expect(async () => {
- const projectFiles = await app.workbench.positronExplorer.getExplorerProjectFiles();
- expect(projectFiles).not.toContain('renv');
- expect(projectFiles).not.toContain('.Rprofile');
- expect(projectFiles).not.toContain('renv.lock');
- }).toPass({ timeout: 50000 });
- });
- });
- });
-
- describe('Jupyter - New Project Wizard', () => {
- const defaultProjectName = 'my-jupyter-notebook';
-
- it('Jupyter Project Defaults [C629352] #pr', async function () {
- const app = this.app as Application;
- const pw = app.workbench.positronNewProjectWizard;
- await pw.startNewProject(ProjectType.JUPYTER_NOTEBOOK);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.navigate(ProjectWizardNavigateAction.NEXT);
- await pw.navigate(ProjectWizardNavigateAction.CREATE);
- await pw.currentOrNewWindowSelectionModal.currentWindowButton.click();
- await app.workbench.positronExplorer.explorerProjectTitle.waitForText(defaultProjectName);
- // NOTE: For completeness, we probably want to await app.workbench.positronConsole.waitForReady('>>>', 10000);
- // here, but it's timing out in CI, so it is not included for now.
- });
- });
-
-});
-
-function addRandomNumSuffix(name: string): string {
- return `${name}_${Math.floor(Math.random() * 1000000)}`;
-}
diff --git a/test/smoke/src/areas/positron/notebook/largePythonNotebook.test.ts b/test/smoke/src/areas/positron/notebook/largePythonNotebook.test.ts
deleted file mode 100644
index 63599e145bd..00000000000
--- a/test/smoke/src/areas/positron/notebook/largePythonNotebook.test.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { join } from 'path';
-import { Application, PositronNotebooks, PositronPythonFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-import { expect } from '@playwright/test';
-
-// Note that this test is too heavy to pass on web and windows
-describe('Large Notebooks', () => {
- setupAndStartApp();
-
- describe('Large Python Notebook', () => {
- let app: Application;
- let notebooks: PositronNotebooks;
-
- before(async function () {
- app = this.app as Application;
- notebooks = app.workbench.positronNotebooks;
- await PositronPythonFixtures.SetupFixtures(app);
- });
-
- it('Python - Large notebook execution [C983592]', async function () {
-
- // huge timeout because this is a heavy test
- this.timeout(480_000);
-
- await app.workbench.positronQuickaccess.openDataFile(join(app.workspacePathOrFolder, 'workspaces', 'large_py_notebook', 'spotify.ipynb'));
-
- await notebooks.selectInterpreter('Python Environments', process.env.POSITRON_PY_VER_SEL!);
-
- await app.code.driver.page.getByText('Run All').click();
-
- const stopExecutionLocator = app.code.driver.page.locator('a').filter({ hasText: 'Stop Execution' });
-
- await expect(stopExecutionLocator).toBeVisible();
- await expect(stopExecutionLocator).not.toBeVisible({ timeout: 120000 });
-
- await app.workbench.quickaccess.runCommand('notebook.focusTop');
-
- await app.code.driver.page.locator('span').filter({ hasText: 'import pandas as pd' }).locator('span').first().click();
-
- const allFigures: any[] = [];
- const uniqueLocators = new Set();
-
- for (let i = 0; i < 6; i++) {
-
- // the second param to wheel (y) seems to be ignored so we send
- // more messages instead of one with a large y value
- for (let j = 0; j < 100; j++) {
- await app.code.driver.page.mouse.wheel(0, 1);
- await app.code.wait(100);
- }
-
- const figureLocator = app.workbench.positronNotebooks.frameLocator.locator('.plot-container');
- const figures = await figureLocator.all();
-
- if (figures!.length > 0) {
- for (const figure of figures!) {
- if (!uniqueLocators.has(figure.toString())) {
- allFigures.push(figure);
- uniqueLocators.add(figure.toString());
- }
- }
- }
- }
-
- expect(allFigures.length).toBeGreaterThan(20);
-
- });
- });
-});
diff --git a/test/smoke/src/areas/positron/notebook/notebook-create.test.ts b/test/smoke/src/areas/positron/notebook/notebook-create.test.ts
new file mode 100644
index 00000000000..7bae63a1c76
--- /dev/null
+++ b/test/smoke/src/areas/positron/notebook/notebook-create.test.ts
@@ -0,0 +1,68 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Notebooks', { tag: ['@pr', '@web', '@win'] }, () => {
+ test.describe('Python Notebooks', () => {
+ test.beforeEach(async function ({ app, python }) {
+ await app.workbench.positronLayouts.enterLayout('notebook');
+ await app.workbench.positronNotebooks.createNewNotebook();
+ await app.workbench.positronNotebooks.selectInterpreter('Python Environments', process.env.POSITRON_PY_VER_SEL!);
+ });
+
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronNotebooks.closeNotebookWithoutSaving();
+ });
+
+ test('Python - Basic notebook creation and execution (code) [C628631]', async function ({ app }) {
+ await app.workbench.positronNotebooks.addCodeToFirstCell('eval("8**2")');
+ await app.workbench.positronNotebooks.executeCodeInCell();
+ await app.workbench.positronNotebooks.assertCellOutput('64');
+ });
+
+ test('Python - Basic notebook creation and execution (markdown) [C628632]', async function ({ app }) {
+ const randomText = Math.random().toString(36).substring(7);
+
+ await app.workbench.notebook.insertNotebookCell('markdown');
+ await app.workbench.notebook.waitForTypeInEditor(`## ${randomText} `);
+ await app.workbench.notebook.stopEditingCell();
+ await app.workbench.positronNotebooks.assertMarkdownText('h2', randomText);
+ });
+ });
+
+ test.describe('R Notebooks', () => {
+ test.beforeEach(async function ({ app, r }) {
+ await app.workbench.positronLayouts.enterLayout('notebook');
+ await app.workbench.positronNotebooks.createNewNotebook();
+ await app.workbench.positronNotebooks.selectInterpreter('R Environments', process.env.POSITRON_R_VER_SEL!);
+ });
+
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronNotebooks.closeNotebookWithoutSaving();
+ });
+
+ test('R - Basic notebook creation and execution (code) [C628629]', async function ({ app }) {
+ await app.workbench.positronNotebooks.addCodeToFirstCell('eval(parse(text="8**2"))');
+ await app.workbench.positronNotebooks.executeCodeInCell();
+ await app.workbench.positronNotebooks.assertCellOutput('[1] 64');
+ });
+
+ test('R - Basic notebook creation and execution (markdown) [C628630]', async function ({ app }) {
+ const randomText = Math.random().toString(36).substring(7);
+
+ await app.workbench.notebook.insertNotebookCell('markdown');
+ await app.workbench.notebook.waitForTypeInEditor(`## ${randomText} `);
+ await app.workbench.notebook.stopEditingCell();
+ await app.workbench.positronNotebooks.assertMarkdownText('h2', randomText);
+ });
+ });
+});
+
+
diff --git a/test/smoke/src/areas/positron/notebook/notebook-large-python.test.ts b/test/smoke/src/areas/positron/notebook/notebook-large-python.test.ts
new file mode 100644
index 00000000000..f10f0dcfc8a
--- /dev/null
+++ b/test/smoke/src/areas/positron/notebook/notebook-large-python.test.ts
@@ -0,0 +1,63 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+
+test.use({
+ suiteId: __filename,
+ snapshots: false,
+});
+
+// Note that this test is too heavy to pass on web and windows
+test.describe('Large Python Notebook', {
+ annotation: [{ type: 'issue', description: 'This test is too heavy to run in CICD due to snapshots on the excessive scrolling.' }],
+}, () => {
+
+ test('Python - Large notebook execution [C983592]', async function ({ app, python }) {
+ test.setTimeout(480_000); // huge timeout because this is a heavy test
+ const notebooks = app.workbench.positronNotebooks;
+
+
+ await app.workbench.positronQuickaccess.openDataFile(join(app.workspacePathOrFolder, 'workspaces', 'large_py_notebook', 'spotify.ipynb'));
+ await notebooks.selectInterpreter('Python Environments', process.env.POSITRON_PY_VER_SEL!);
+
+ await app.code.driver.page.getByText('Run All').click();
+
+ const stopExecutionLocator = app.code.driver.page.locator('a').filter({ hasText: 'Stop Execution' });
+ await expect(stopExecutionLocator).toBeVisible();
+ await expect(stopExecutionLocator).not.toBeVisible({ timeout: 120000 });
+
+ await app.workbench.quickaccess.runCommand('notebook.focusTop');
+ await app.code.driver.page.locator('span').filter({ hasText: 'import pandas as pd' }).locator('span').first().click();
+
+ const allFigures: any[] = [];
+ const uniqueLocators = new Set();
+
+ for (let i = 0; i < 6; i++) {
+
+ // the second param to wheel (y) seems to be ignored so we send
+ // more messages instead of one with a large y value
+ for (let j = 0; j < 100; j++) {
+ await app.code.driver.page.mouse.wheel(0, 1);
+ await app.code.driver.page.waitForTimeout(100);
+ }
+
+ const figureLocator = app.workbench.positronNotebooks.frameLocator.locator('.plot-container');
+ const figures = await figureLocator.all();
+
+ if (figures!.length > 0) {
+ for (const figure of figures!) {
+ if (!uniqueLocators.has(figure.toString())) {
+ allFigures.push(figure);
+ uniqueLocators.add(figure.toString());
+ }
+ }
+ }
+ }
+
+ expect(allFigures.length).toBeGreaterThan(20);
+ });
+});
diff --git a/test/smoke/src/areas/positron/notebook/notebookCreate.test.ts b/test/smoke/src/areas/positron/notebook/notebookCreate.test.ts
deleted file mode 100644
index cecc3265600..00000000000
--- a/test/smoke/src/areas/positron/notebook/notebookCreate.test.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { Application, PositronNotebooks, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Notebooks #pr #web #win', () => {
- setupAndStartApp();
-
- describe('R Notebooks', () => {
- let app: Application;
- let notebooks: PositronNotebooks;
-
- before(async function () {
- app = this.app as Application;
- notebooks = app.workbench.positronNotebooks;
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- beforeEach(async function () {
- await notebooks.createNewNotebook();
- await notebooks.selectInterpreter('R Environments', process.env.POSITRON_R_VER_SEL!);
- });
-
- afterEach(async function () {
- await notebooks.closeNotebookWithoutSaving();
- });
-
- it('R - Basic notebook creation and execution (code) [C628629]', async function () {
-
- this.retries(1);
-
- await notebooks.addCodeToFirstCell('eval(parse(text="8**2"))');
- await notebooks.executeCodeInCell();
- await notebooks.assertCellOutput('[1] 64');
- });
-
- it('R - Basic notebook creation and execution (markdown) [C628630]', async function () {
-
- this.retries(1);
-
- const randomText = Math.random().toString(36).substring(7);
-
- await app.workbench.notebook.insertNotebookCell('markdown');
- await app.workbench.notebook.waitForTypeInEditor(`## ${randomText} `);
- await app.workbench.notebook.stopEditingCell();
- await notebooks.assertMarkdownText('h2', randomText);
- });
- });
-
- describe('Python Notebooks', () => {
- let app: Application;
- let notebooks: PositronNotebooks;
-
- before(async function () {
- app = this.app as Application;
- notebooks = app.workbench.positronNotebooks;
- await PositronPythonFixtures.SetupFixtures(app);
- });
-
- beforeEach(async function () {
- await notebooks.createNewNotebook();
- await notebooks.selectInterpreter('Python Environments', process.env.POSITRON_PY_VER_SEL!);
- });
-
- afterEach(async function () {
- await notebooks.closeNotebookWithoutSaving();
- });
-
- it('Python - Basic notebook creation and execution (code) [C628631]', async function () {
-
- this.retries(1);
-
- await notebooks.addCodeToFirstCell('eval("8**2")');
- await notebooks.executeCodeInCell();
- await notebooks.assertCellOutput('64');
- });
-
- it('Python - Basic notebook creation and execution (markdown) [C628632]', async function () {
-
- this.retries(1);
-
- const randomText = Math.random().toString(36).substring(7);
-
- await app.workbench.notebook.insertNotebookCell('markdown');
- await app.workbench.notebook.waitForTypeInEditor(`## ${randomText} `);
- await app.workbench.notebook.stopEditingCell();
- await notebooks.assertMarkdownText('h2', randomText);
- });
- });
-
-
-});
diff --git a/test/smoke/src/areas/positron/outline/outline.test.ts b/test/smoke/src/areas/positron/outline/outline.test.ts
index 51b66ae33b3..10657c26e7d 100644
--- a/test/smoke/src/areas/positron/outline/outline.test.ts
+++ b/test/smoke/src/areas/positron/outline/outline.test.ts
@@ -3,68 +3,51 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-
import { join } from 'path';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-
-describe('Outline #web #win', () => {
- setupAndStartApp();
-
- describe('Outline Test - Python', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- it('Python - Verify Outline Contents [C956870]', async function () {
- const app = this.app as Application;
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'chinook-db-py', 'chinook-sqlite.py'));
-
- const outlineData = await app.workbench.positronOutline.getOutlineData();
-
- const expected = [
- 'data_file_pathdata_file_path = os.path.join(os.getcwd(), \'data-files\', \'chinook\', \'chinook.db\')',
- 'connconn = sqlite3.connect(data_file_path)',
- 'curcur = conn.cursor()',
- 'rowsrows = cur.fetchall()',
- 'dfdf = pd.DataFrame(rows)'
- ];
+import { test } from '../_test.setup';
- const missingFromUI = expected.filter(item => !outlineData.includes(item));
+test.use({
+ suiteId: __filename
+});
- if (missingFromUI.length > 0) {
- console.log(`Missing from UI: ${missingFromUI}`);
- }
- });
+test.describe('Outline #web #win', {
+ tag: ['@web', '@win']
+}, () => {
+
+ test('Python - Verify Outline Contents [C956870]', async function ({ app, python }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'chinook-db-py', 'chinook-sqlite.py'));
+ const outlineData = await app.workbench.positronOutline.getOutlineData();
+ const expected = [
+ 'data_file_pathdata_file_path = os.path.join(os.getcwd(), \'data-files\', \'chinook\', \'chinook.db\')',
+ 'connconn = sqlite3.connect(data_file_path)',
+ 'curcur = conn.cursor()',
+ 'rowsrows = cur.fetchall()',
+ 'dfdf = pd.DataFrame(rows)'
+ ];
+
+ const missingFromUI = expected.filter(item => !outlineData.includes(item));
+
+ if (missingFromUI.length > 0) {
+ console.log(`Missing from UI: ${missingFromUI}`);
+ }
});
+ test('R - Verify Outline Contents [C956871]', async function ({ app, r }) {
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'chinook-db-r', 'chinook-sqlite.r'));
+ const outlineData = await app.workbench.positronOutline.getOutlineData();
+ const expected = [
+ 'con',
+ 'albums',
+ 'df',
+ ];
+ const missingFromUI = expected.filter(item => !outlineData.includes(item));
- describe('Outline Test - R', () => {
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- it('R - Verify Outline Contents [C956871]', async function () {
- const app = this.app as Application;
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'chinook-db-r', 'chinook-sqlite.r'));
-
- const outlineData = await app.workbench.positronOutline.getOutlineData();
-
- const expected = [
- 'con',
- 'albums',
- 'df',
- ];
-
- const missingFromUI = expected.filter(item => !outlineData.includes(item));
-
- if (missingFromUI.length > 0) {
- console.log(`Missing from UI: ${missingFromUI}`);
- }
- });
+ if (missingFromUI.length > 0) {
+ console.log(`Missing from UI: ${missingFromUI}`);
+ }
});
});
+
diff --git a/test/smoke/src/areas/positron/output/console-ouput-log.test.ts b/test/smoke/src/areas/positron/output/console-ouput-log.test.ts
new file mode 100644
index 00000000000..1c0ef2ea711
--- /dev/null
+++ b/test/smoke/src/areas/positron/output/console-ouput-log.test.ts
@@ -0,0 +1,40 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { test } from '../_test.setup';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Console Output Log', { tag: ['@web'] }, () => {
+ test.beforeEach(async function ({ app }) {
+ await app.workbench.positronLayouts.enterLayout('stacked');
+ });
+
+ test('Python - Verify Console Output Log Contents [C667518]', async function ({ app, python }) {
+ const activeConsole = app.workbench.positronConsole.activeConsole;
+ await activeConsole.click();
+
+ await app.workbench.positronConsole.typeToConsole('a = b');
+ await app.workbench.positronConsole.sendEnterKey();
+
+ await app.workbench.positronOutput.clickOutputTab();
+ await app.workbench.positronLayouts.enterLayout('fullSizedPanel');
+ await app.workbench.positronOutput.waitForOutContaining("name 'b' is not defined");
+ });
+
+ test('R - Verify Console Output Log Contents [C667519]', async function ({ app, r }) {
+ const activeConsole = app.workbench.positronConsole.activeConsole;
+ await activeConsole.click();
+
+ await app.workbench.positronConsole.typeToConsole('a = b');
+ await app.workbench.positronConsole.sendEnterKey();
+
+ await app.workbench.positronOutput.clickOutputTab();
+ await app.workbench.positronLayouts.enterLayout('fullSizedPanel');
+ await app.workbench.positronOutput.waitForOutContaining("object 'b' not found");
+ });
+});
diff --git a/test/smoke/src/areas/positron/output/consoleOutputLog.test.ts b/test/smoke/src/areas/positron/output/consoleOutputLog.test.ts
deleted file mode 100644
index c79408f0599..00000000000
--- a/test/smoke/src/areas/positron/output/consoleOutputLog.test.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-
-describe('Output #web', () => {
- setupAndStartApp();
-
- describe('Console Output Log - Python', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- after(async function () {
- const app = this.app as Application;
- await app.workbench.positronLayouts.enterLayout('stacked');
- });
-
- it('Python - Verify Console Output Log Contents [C667518]', async function () {
- const app = this.app as Application;
-
- const activeConsole = app.workbench.positronConsole.activeConsole;
- await activeConsole.click();
-
- await app.workbench.positronConsole.typeToConsole('a = b');
- await app.workbench.positronConsole.sendEnterKey();
-
- // retry in case the console output log is slow to appear
- await expect(async () => {
- await app.workbench.positronOutput.openOutputPane(process.env.POSITRON_PY_VER_SEL!);
-
- await app.workbench.positronLayouts.enterLayout('fullSizedPanel');
-
- await app.workbench.positronOutput.waitForOutContaining("name 'b' is not defined");
- }).toPass({ timeout: 60000 });
- });
- });
-});
-
-describe('Output #web', () => {
- setupAndStartApp();
-
- describe('Console Output Log - R', () => {
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- after(async function () {
- const app = this.app as Application;
- await app.workbench.positronLayouts.enterLayout('stacked');
- });
-
- it('R - Verify Console Output Log Contents [C667519]', async function () {
- const app = this.app as Application;
-
- const activeConsole = app.workbench.positronConsole.activeConsole;
- await activeConsole.click();
-
- await app.workbench.positronConsole.typeToConsole('a = b');
- await app.workbench.positronConsole.sendEnterKey();
-
- // retry in case the console output log is slow to appear
- await expect(async () => {
- await app.workbench.positronOutput.openOutputPane(process.env.POSITRON_R_VER_SEL!);
-
- await app.workbench.positronLayouts.enterLayout('fullSizedPanel');
-
- await app.workbench.positronOutput.waitForOutContaining("object 'b' not found");
- }).toPass({ timeout: 60000 });
-
- });
- });
-});
-
diff --git a/test/smoke/plots/autos.png b/test/smoke/src/areas/positron/plots/autos.png
similarity index 100%
rename from test/smoke/plots/autos.png
rename to test/smoke/src/areas/positron/plots/autos.png
diff --git a/test/smoke/plots/graphviz.png b/test/smoke/src/areas/positron/plots/graphviz.png
similarity index 100%
rename from test/smoke/plots/graphviz.png
rename to test/smoke/src/areas/positron/plots/graphviz.png
diff --git a/test/smoke/src/areas/positron/plots/plots.test.ts b/test/smoke/src/areas/positron/plots/plots.test.ts
index dc6d2f49fcc..569db662db5 100644
--- a/test/smoke/src/areas/positron/plots/plots.test.ts
+++ b/test/smoke/src/areas/positron/plots/plots.test.ts
@@ -3,99 +3,44 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-
-import { expect } from '@playwright/test';
import * as path from 'path';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
import compareImages = require('resemblejs/compareImages');
import { ComparisonOptions } from 'resemblejs';
import * as fs from 'fs';
import { fail } from 'assert';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-let logger;
+import { test, expect } from '../_test.setup';
+import { Application } from '../../../../../automation';
-const diffPlotsPath = ['..', '..', '.build', 'logs', 'smoke-tests-electron'];
-const options: ComparisonOptions = {
- output: {
- errorColor: {
- red: 255,
- green: 0,
- blue: 255
- },
- errorType: 'movement',
- transparency: 0.3,
- largeImageThreshold: 1200,
- useCrossOrigin: false
- },
- scaleToSameSize: true,
- ignore: 'antialiasing',
-};
-const githubActions = process.env.GITHUB_ACTIONS === "true";
+test.use({
+ suiteId: __filename
+});
// web bugs 4800 & 4804
-describe('Plots', () => {
- logger = setupAndStartApp();
-
- async function simplePlotTest(app: Application, script: string, locator: string, RWeb = false) {
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
- await app.workbench.positronPlots.clearPlots();
- await app.workbench.positronPlots.waitForNoPlots();
- await app.workbench.positronLayouts.enterLayout('stacked');
-
- await app.workbench.positronConsole.pasteCodeToConsole(script);
- await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
- await app.workbench.positronPlots.waitForWebviewPlot(locator, 'visible', RWeb);
- await app.workbench.positronLayouts.enterLayout('stacked');
- }
+test.describe('Plots', () => {
+ test.describe('Python Plots', () => {
+ test.beforeAll(async function ({ app, interpreter }) {
+ await interpreter.set('Python');
- describe('Python Plots', () => {
-
- before(async function () {
// Set the viewport to a size that ensures all the plots view actions are visible
if (process.platform === 'linux') {
- await this.app.code.driver.setViewportSize({ width: 1280, height: 800 });
+ await app.code.driver.setViewportSize({ width: 1280, height: 800 });
}
- await this.app.workbench.positronLayouts.enterLayout('stacked');
-
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
+ await app.workbench.positronLayouts.enterLayout('stacked');
});
- it('Python - Verifies basic plot functionality - Dynamic Plot [C608114] #pr #web', async function () {
- const app = this.app as Application;
-
+ test('Python - Verifies basic plot functionality - Dynamic Plot [C608114]', {
+ tag: ['@pr', '@web']
+ }, async function ({ app, logger }) {
// modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/
- const script = `import pandas as pd
-import matplotlib.pyplot as plt
-data_dict = {'name': ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'],
- 'age': [20, 20, 21, 20, 21, 20],
- 'math_marks': [100, 90, 91, 98, 92, 95],
- 'physics_marks': [90, 100, 91, 92, 98, 95],
- 'chem_marks': [93, 89, 99, 92, 94, 92]
- }
-
-df = pd.DataFrame(data_dict)
-
-df.plot(kind='scatter',
- x='math_marks',
- y='physics_marks',
- color='red')
-
-plt.title('ScatterPlot')
-plt.show()`;
-
logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('Python', script, '>>>');
-
+ await app.workbench.positronConsole.executeCode('Python', pythonDynamicPlot, '>>>');
await app.workbench.positronPlots.waitForCurrentPlot();
const buffer = await app.workbench.positronPlots.getCurrentPlotAsBuffer();
+ // const temp = path.join('plots', 'pythonScatterplot.png');
+ const data = await compareImages(fs.readFileSync(path.join(__dirname, 'pythonScatterplot.png')), buffer, options);
- const data = await compareImages(fs.readFileSync(path.join('plots', 'pythonScatterplot.png'),), buffer, options);
-
- if (githubActions && !this.app.web && data.rawMisMatchPercentage > 2.0) {
+ if (githubActions && !app.web && data.rawMisMatchPercentage > 2.0) {
if (data.getBuffer) {
// FIXME: Temporarily ignore compilation issue
// See "Type 'Buffer' is not assignable" errors on https://github.com/microsoft/TypeScript/issues/59451
@@ -111,39 +56,18 @@ plt.show()`;
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
await app.workbench.positronPlots.clearPlots();
await app.workbench.positronLayouts.enterLayout('stacked');
-
await app.workbench.positronPlots.waitForNoPlots();
});
- it('Python - Verifies basic plot functionality - Static Plot [C654401] #pr #web', async function () {
- const app = this.app as Application;
-
- const script = `import graphviz as gv
-import IPython
-
-h = gv.Digraph(format="svg")
-names = [
- "A",
- "B",
- "C",
-]
-
-# Specify edges
-h.edge("A", "B")
-h.edge("A", "C")
-
-IPython.display.display_png(h)`;
-
+ test('Python - Verifies basic plot functionality - Static Plot [C654401]', { tag: ['@pr', '@web'] }, async function ({ app, logger }) {
logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('Python', script, '>>>');
-
+ await app.workbench.positronConsole.executeCode('Python', pythonStaticPlot, '>>>');
await app.workbench.positronPlots.waitForCurrentStaticPlot();
const buffer = await app.workbench.positronPlots.getCurrentStaticPlotAsBuffer();
+ const data = await compareImages(fs.readFileSync(path.join(__dirname, 'graphviz.png'),), buffer, options);
- const data = await compareImages(fs.readFileSync(path.join('plots', 'graphviz.png'),), buffer, options);
-
- if (githubActions && !this.app.web && data.rawMisMatchPercentage > 2.0) {
+ if (githubActions && !app.web && data.rawMisMatchPercentage > 2.0) {
if (data.getBuffer) {
// FIXME: Temporarily ignore compilation issue
// See "Type 'Buffer' is not assignable" errors on https://github.com/microsoft/TypeScript/issues/59451
@@ -152,58 +76,16 @@ IPython.display.display_png(h)`;
}
// capture a new master image in CI
await app.workbench.positronPlots.currentPlot.screenshot({ path: path.join(...diffPlotsPath, 'graphviz.png') });
-
fail(`Image comparison failed with mismatch percentage: ${data.rawMisMatchPercentage}`);
}
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
await app.workbench.positronPlots.clearPlots();
await app.workbench.positronLayouts.enterLayout('stacked');
-
await app.workbench.positronPlots.waitForNoPlots();
});
- it('Python - Verifies the plots pane action bar - Plot actions [C656297] #web #win', async function () {
- const app = this.app as Application;
-
- const scriptPlot1 = `import graphviz as gv
-import IPython
-
-h = gv.Digraph(format="svg")
-names = [
- "A",
- "B",
- "C",
-]
-
-# Specify edges
-h.edge("A", "B")
-h.edge("A", "C")
-
-IPython.display.display_png(h)`;
-
- const scriptPlot2 = `import matplotlib.pyplot as plt
-
-# x axis values
-x = [1,2,3]
-# corresponding y axis values
-y = [2,4,1]
-
-# plotting the points
-plt.plot(x, y)
-
-# naming the x axis
-plt.xlabel('x - axis')
-# naming the y axis
-plt.ylabel('y - axis')
-
-# giving a title to my graph
-plt.title('My first graph!')
-
-# function to show the plot
-plt.show()`;
- logger.log('Sending code to console');
-
+ test('Python - Verifies the plots pane action bar - Plot actions [C656297]', { tag: ['@web', '@win'] }, async function ({ app }) {
// default plot pane state for action bar
await expect(app.workbench.positronPlots.plotSizeButton).not.toBeVisible();
await expect(app.workbench.positronPlots.savePlotButton).not.toBeVisible();
@@ -211,14 +93,13 @@ plt.show()`;
await expect(app.workbench.positronPlots.zoomPlotButton).not.toBeVisible();
// create plots separately so that the order is known
- await app.workbench.positronConsole.executeCode('Python', scriptPlot1, '>>>');
+ await app.workbench.positronConsole.executeCode('Python', pythonPlotActions1, '>>>');
await app.workbench.positronPlots.waitForCurrentStaticPlot();
- await app.workbench.positronConsole.executeCode('Python', scriptPlot2, '>>>');
+ await app.workbench.positronConsole.executeCode('Python', pythonPlotActions2, '>>>');
await app.workbench.positronPlots.waitForCurrentPlot();
// expand the plot pane to show the action bar
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
-
await expect(app.workbench.positronPlots.clearPlotsButton).not.toBeDisabled();
await expect(app.workbench.positronPlots.nextPlotButton).toBeDisabled();
await expect(app.workbench.positronPlots.previousPlotButton).not.toBeDisabled();
@@ -233,7 +114,6 @@ plt.show()`;
// switching to fized size plot changes action bar
await expect(app.workbench.positronPlots.zoomPlotButton).toBeVisible();
await expect(app.workbench.positronPlots.plotSizeButton).not.toBeVisible();
-
await expect(app.workbench.positronPlots.clearPlotsButton).not.toBeDisabled();
await expect(app.workbench.positronPlots.nextPlotButton).not.toBeDisabled();
await expect(app.workbench.positronPlots.previousPlotButton).toBeDisabled();
@@ -242,10 +122,8 @@ plt.show()`;
// switch back to dynamic plot
await app.workbench.positronPlots.nextPlotButton.click();
await app.workbench.positronPlots.waitForCurrentPlot();
-
await expect(app.workbench.positronPlots.zoomPlotButton).toBeVisible();
await expect(app.workbench.positronPlots.plotSizeButton).toBeVisible();
-
await expect(app.workbench.positronPlots.clearPlotsButton).not.toBeDisabled();
await expect(app.workbench.positronPlots.nextPlotButton).toBeDisabled();
await expect(app.workbench.positronPlots.previousPlotButton).not.toBeDisabled();
@@ -253,43 +131,17 @@ plt.show()`;
await app.workbench.positronPlots.clearPlots();
await app.workbench.positronLayouts.enterLayout('stacked');
-
await app.workbench.positronPlots.waitForNoPlots();
});
- it('Python - Verifies saving a Python plot [C557005]', async function () {
- const app = this.app as Application;
-
- // modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/
- const script = `import pandas as pd
-import matplotlib.pyplot as plt
-data_dict = {'name': ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'],
- 'age': [20, 20, 21, 20, 21, 20],
- 'math_marks': [100, 90, 91, 98, 92, 95],
- 'physics_marks': [90, 100, 91, 92, 98, 95],
- 'chem_marks': [93, 89, 99, 92, 94, 92]
- }
-
-df = pd.DataFrame(data_dict)
-
-df.plot(kind='scatter',
- x='math_marks',
- y='physics_marks',
- color='red')
-
-plt.title('ScatterPlot')
-plt.show()`;
-
+ test('Python - Verifies saving a Python plot [C557005]', async function ({ app, logger }) {
logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('Python', script, '>>>');
-
+ await app.workbench.positronConsole.executeCode('Python', savePlot, '>>>');
await app.workbench.positronPlots.waitForCurrentPlot();
-
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
// save again with a different name and file format
await app.workbench.positronPlots.savePlotButton.click();
-
await app.workbench.positronPopups.waitForModalDialogBox();
// fill in the file name and change file format to JPEG
@@ -303,100 +155,30 @@ plt.show()`;
// verify the plot is in the file explorer with the new file name and format
await app.workbench.positronLayouts.enterLayout('stacked');
await app.workbench.positronExplorer.waitForProjectFileToAppear('Python-scatter.jpeg');
-
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
await app.workbench.positronPlots.clearPlots();
await app.workbench.positronLayouts.enterLayout('stacked');
-
await app.workbench.positronPlots.waitForNoPlots();
});
- it('Python - Verifies bqplot Python widget [C720869] #web', async function () {
- const app = this.app as Application;
-
- const script = `import bqplot.pyplot as bplt
-import numpy as np
-
-x = np.linspace(-10, 10, 100)
-y = np.sin(x)
-axes_opts = {"x": {"label": "X"}, "y": {"label": "Y"}}
-
-fig = bplt.figure(title="Line Chart")
-line = bplt.plot(
- x=x, y=y, axes_options=axes_opts
-)
-
-bplt.show()`;
-
- await simplePlotTest(app, script, '.svg-figure');
-
+ test('Python - Verifies bqplot Python widget [C720869]', { tag: ['@web'] }, async function ({ app }) {
+ await simplePlotTest(app, bgplot, '.svg-figure');
});
- it('Python - Verifies ipydatagrid Python widget [C720870] #web #win', async function () {
- const app = this.app as Application;
-
- const script = `import pandas as pd
-from ipydatagrid import DataGrid
-data= pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, index=["One", "Two", "Three"])
-DataGrid(data)
-DataGrid(data, selection_mode="cell", editable=True)`;
-
- await simplePlotTest(app, script, 'canvas:nth-child(1)');
-
+ test('Python - Verifies ipydatagrid Python widget [C720870]', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await simplePlotTest(app, ipydatagrid, 'canvas:nth-child(1)');
});
- it('Python - Verifies ipyleaflet Python widget [C720871] #web #win', async function () {
- const app = this.app as Application;
-
- const script = `from ipyleaflet import Map, Marker, display
-center = (52.204793, 360.121558)
-map = Map(center=center, zoom=12)
-
-# Add a draggable marker to the map
-# Dragging the marker updates the marker.location value in Python
-marker = Marker(location=center, draggable=True)
-map.add_control(marker)
-
-display(map)`;
-
- await simplePlotTest(app, script, '.leaflet-container');
+ test('Python - Verifies ipyleaflet Python widget [C720871]', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await simplePlotTest(app, ipyleaflet, '.leaflet-container');
});
- it('Python - Verifies hvplot can load with plotly extension [C766660] #web #win', async function () {
- const app = this.app as Application;
-
- const script = `import hvplot.pandas
-import pandas as pd
-hvplot.extension('plotly')
-pd.DataFrame(dict(x=[1,2,3], y=[4,5,6])).hvplot.scatter(x="x", y="y")`;
-
- await simplePlotTest(app, script, '.plotly');
+ test('Python - Verifies hvplot can load with plotly extension [C766660]', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await simplePlotTest(app, plotly, '.plotly');
});
- it('Python - Verifies ipytree Python widget [C720872] #web #win', async function () {
- const app = this.app as Application;
-
- const script = `from ipytree import Tree, Node
-tree = Tree(stripes=True)
-tree
-tree
-node1 = Node('node1')
-tree.add_node(node1)
-node2 = Node('node2')
-tree.add_node(node2)
-tree.nodes = [node2, node1]
-node3 = Node('node3', disabled=True)
-node4 = Node('node4')
-node5 = Node('node5', [Node('1'), Node('2')])
-node2.add_node(node3)
-node2.add_node(node4)
-node2.add_node(node5)
-tree.add_node(Node('node6'), 1)
-node2.add_node(Node('node7'), 2)
-
-tree`;
-
- await simplePlotTest(app, script, '.jstree-container-ul');
+ test('Python - Verifies ipytree Python widget [C720872]', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await simplePlotTest(app, ipytree, '.jstree-container-ul');
// fullauxbar layout needed for some smaller windows
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
@@ -406,22 +188,15 @@ tree`;
await expect(treeNodes).toHaveCount(9);
// collapse the tree, only parent nodes should be visible
- treeNodes.first().click({ position: { x: 0, y: 0 } }); // target the + icon
+ await treeNodes.first().click({ position: { x: 0, y: 0 } }); // target the + icon
await expect(treeNodes).toHaveCount(3);
// return to stacked layout
await app.workbench.positronLayouts.enterLayout('stacked');
});
-
- it('Python - Verifies ipywidget.Output Python widget #web #win', async function () {
- const app = this.app as Application;
-
- // Create the Output widget.
- const script = `import ipywidgets
-output = ipywidgets.Output()
-output`;
- await app.workbench.positronConsole.pasteCodeToConsole(script);
+ test('Python - Verifies ipywidget.Output Python widget', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await app.workbench.positronConsole.pasteCodeToConsole(ipywidgetOutput);
await app.workbench.positronConsole.sendEnterKey();
await app.workbench.positronPlots.waitForWebviewPlot('.widget-output', 'attached');
@@ -444,38 +219,17 @@ output`;
});
-
- it('Python - Verifies bokeh Python widget [C730343] #web', async function () {
- const app = this.app as Application;
-
- const script = `from bokeh.plotting import figure, output_file, show
-
-# instantiating the figure object
-graph = figure(title = "Bokeh Line Graph")
-
-# the points to be plotted
-x = [1, 2, 3, 4, 5]
-y = [5, 4, 3, 2, 1]
-
-# plotting the line graph
-graph.line(x, y)
-
-# displaying the model
-show(graph)`;
-
-
- await app.workbench.positronConsole.pasteCodeToConsole(script);
+ test('Python - Verifies bokeh Python widget [C730343]', { tag: ['@web'] }, async function ({ app }) {
+ await app.workbench.positronConsole.pasteCodeToConsole(bokeh);
await app.workbench.positronConsole.sendEnterKey();
// selector not factored out as it is unique to bokeh
const bokehCanvas = '.bk-Canvas';
await app.workbench.positronPlots.waitForWebviewPlot(bokehCanvas);
-
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
// selector not factored out as it is unique to bokeh
await app.workbench.positronPlots.getWebviewPlotLocator('.bk-tool-icon-box-zoom').click();
-
const canvasLocator = app.workbench.positronPlots.getWebviewPlotLocator(bokehCanvas);
const boundingBox = await canvasLocator.boundingBox();
@@ -483,7 +237,6 @@ show(graph)`;
const bufferBeforeZoom = await canvasLocator.screenshot();
if (boundingBox) {
-
await app.code.driver.clickAndDrag({
from: {
x: boundingBox.x + boundingBox.width / 3,
@@ -508,40 +261,24 @@ show(graph)`;
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
await app.workbench.positronPlots.clearPlots();
await app.workbench.positronLayouts.enterLayout('stacked');
-
await app.workbench.positronPlots.waitForNoPlots();
-
await app.workbench.positronLayouts.enterLayout('stacked');
-
});
-
});
- describe('R Plots', () => {
-
- before(async function () {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
+ test.describe('R Plots', () => {
+ test.beforeEach(async function ({ interpreter }) {
+ await interpreter.set('R');
});
- it('R - Verifies basic plot functionality [C628633] #pr #web', async function () {
- const app = this.app as Application;
-
- const script = `cars <- c(1, 3, 6, 4, 9)
-plot(cars, type="o", col="blue")
-title(main="Autos", col.main="red", font.main=4)`;
-
+ test('R - Verifies basic plot functionality [C628633]', { tag: ['@pr', '@web'] }, async function ({ app, logger }) {
logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('R', script, '>');
-
+ await app.workbench.positronConsole.executeCode('R', rBasicPlot, '>');
await app.workbench.positronPlots.waitForCurrentPlot();
-
const buffer = await app.workbench.positronPlots.getCurrentPlotAsBuffer();
+ const data = await compareImages(fs.readFileSync(path.join(__dirname, 'autos.png'),), buffer, options);
- const data = await compareImages(fs.readFileSync(path.join('plots', 'autos.png'),), buffer, options);
-
- if (githubActions && !this.app.web && data.rawMisMatchPercentage > 2.0) {
+ if (githubActions && !app.web && data.rawMisMatchPercentage > 2.0) {
if (data.getBuffer) {
// FIXME: Temporarily ignore compilation issue
// See "Type 'Buffer' is not assignable" errors on https://github.com/microsoft/TypeScript/issues/59451
@@ -557,26 +294,17 @@ title(main="Autos", col.main="red", font.main=4)`;
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
await app.workbench.positronPlots.clearPlots();
await app.workbench.positronLayouts.enterLayout('stacked');
-
await app.workbench.positronPlots.waitForNoPlots();
});
- it('R - Verifies saving an R plot [C557006]', async function () {
- const app = this.app as Application;
-
- const script = `cars <- c(1, 3, 6, 4, 9)
-plot(cars, type="o", col="blue")
-title(main="Autos", col.main="red", font.main=4)`;
-
+ test('R - Verifies saving an R plot [C557006]', async function ({ app, logger }) {
logger.log('Sending code to console');
// create a plot
- await app.workbench.positronConsole.executeCode('R', script, '>');
-
+ await app.workbench.positronConsole.executeCode('R', rSavePlot, '>');
await app.workbench.positronPlots.waitForCurrentPlot();
// click save to bring up the modal save dialog
await app.workbench.positronPlots.savePlotButton.click();
-
await app.workbench.positronPopups.waitForModalDialogBox();
// save with defaults
@@ -587,7 +315,6 @@ title(main="Autos", col.main="red", font.main=4)`;
// save again with a different name and file format
await app.workbench.positronPlots.savePlotButton.click();
-
await app.workbench.positronPopups.waitForModalDialogBox();
// fill in the file name and change file format to SVG
@@ -600,78 +327,267 @@ title(main="Autos", col.main="red", font.main=4)`;
// verify the plot is in the file explorer with the new file name and format
await app.workbench.positronExplorer.waitForProjectFileToAppear('R-cars.svg');
-
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
await app.workbench.positronPlots.clearPlots();
await app.workbench.positronLayouts.enterLayout('stacked');
+ await app.workbench.positronPlots.waitForNoPlots();
+ });
+ test('R - Verifies rplot plot [C720873]', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await app.workbench.positronConsole.pasteCodeToConsole(rplot);
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronPlots.waitForCurrentPlot();
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ await app.workbench.positronPlots.clearPlots();
+ await app.workbench.positronLayouts.enterLayout('stacked');
await app.workbench.positronPlots.waitForNoPlots();
});
+ test('R - Verifies highcharter plot [C720874]', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await simplePlotTest(app, highcharter, 'svg', app.web);
+ });
- it('R - Verifies rplot plot [C720873] #web #win', async function () {
- const app = this.app as Application;
+ test('R - Verifies leaflet plot [C720875]', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await simplePlotTest(app, leaflet, '.leaflet', app.web);
+ });
- const script = `library('corrr')
+ test('R - Verifies plotly plot [C720876]', { tag: ['@web', '@win'] }, async function ({ app }) {
+ await simplePlotTest(app, rPlotly, '.plot-container', app.web);
+ });
+ });
+});
-x <- correlate(mtcars)
-rplot(x)
+const diffPlotsPath = ['..', '..', '.build', 'logs', 'smoke-tests-electron'];
+const options: ComparisonOptions = {
+ output: {
+ errorColor: {
+ red: 255,
+ green: 0,
+ blue: 255
+ },
+ errorType: 'movement',
+ transparency: 0.3,
+ largeImageThreshold: 1200,
+ useCrossOrigin: false
+ },
+ scaleToSameSize: true,
+ ignore: 'antialiasing',
+};
+const githubActions = process.env.GITHUB_ACTIONS === "true";
-# Common use is following rearrange and shave
-x <- rearrange(x, absolute = FALSE)
-x <- shave(x)
-rplot(x)
-rplot(x, print_cor = TRUE)
-rplot(x, shape = 20, colors = c("red", "green"), legend = TRUE)`;
+async function simplePlotTest(app: Application, script: string, locator: string, RWeb = false) {
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ await app.workbench.positronPlots.clearPlots();
+ await app.workbench.positronPlots.waitForNoPlots();
+ await app.workbench.positronLayouts.enterLayout('stacked');
- await app.workbench.positronConsole.pasteCodeToConsole(script);
- await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronPlots.waitForCurrentPlot();
+ await app.workbench.positronConsole.pasteCodeToConsole(script);
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ await app.workbench.positronPlots.waitForWebviewPlot(locator, 'visible', RWeb);
+ await app.workbench.positronLayouts.enterLayout('stacked');
+}
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
- await app.workbench.positronPlots.clearPlots();
- await app.workbench.positronLayouts.enterLayout('stacked');
+const pythonDynamicPlot = `import pandas as pd
+import matplotlib.pyplot as plt
+data_dict = {'name': ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'],
+ 'age': [20, 20, 21, 20, 21, 20],
+ 'math_marks': [100, 90, 91, 98, 92, 95],
+ 'physics_marks': [90, 100, 91, 92, 98, 95],
+ 'chem_marks': [93, 89, 99, 92, 94, 92]
+ }
- await app.workbench.positronPlots.waitForNoPlots();
+df = pd.DataFrame(data_dict)
- });
+df.plot(kind='scatter',
+ x='math_marks',
+ y='physics_marks',
+ color='red')
- it('R - Verifies highcharter plot [C720874] #web #win', async function () {
- const app = this.app as Application;
+plt.title('ScatterPlot')
+plt.show()`;
- const script = `library(highcharter)
-data("mpg", "diamonds", "economics_long", package = "ggplot2")
+const pythonStaticPlot = `import graphviz as gv
+import IPython
-hchart(mpg, "point", hcaes(x = displ, y = cty, group = year))`;
+h = gv.Digraph(format="svg")
+names = [
+ "A",
+ "B",
+ "C",
+]
- await simplePlotTest(app, script, 'svg', this.app.web);
+# Specify edges
+h.edge("A", "B")
+h.edge("A", "C")
- });
+IPython.display.display_png(h)`;
- it('R - Verifies leaflet plot [C720875] #web @win', async function () {
- const app = this.app as Application;
+const pythonPlotActions1 = `import graphviz as gv
+import IPython
- const script = `library(leaflet)
-m = leaflet() %>% addTiles()
-m = m %>% setView(-93.65, 42.0285, zoom = 17)
-m %>% addPopups(-93.65, 42.0285, 'Here is the Department of Statistics, ISU')`;
+h = gv.Digraph(format="svg")
+names = [
+ "A",
+ "B",
+ "C",
+]
- await simplePlotTest(app, script, '.leaflet', this.app.web);
+# Specify edges
+h.edge("A", "B")
+h.edge("A", "C")
- });
+IPython.display.display_png(h)`;
- it('R - Verifies plotly plot [C720876] #web #win', async function () {
- const app = this.app as Application;
+const pythonPlotActions2 = `import matplotlib.pyplot as plt
- const script = `library(plotly)
-fig <- plot_ly(midwest, x = ~percollege, color = ~state, type = "box")
-fig`;
+# x axis values
+x = [1,2,3]
+# corresponding y axis values
+y = [2,4,1]
- await simplePlotTest(app, script, '.plot-container', this.app.web);
+# plotting the points
+plt.plot(x, y)
- });
+# naming the x axis
+plt.xlabel('x - axis')
+# naming the y axis
+plt.ylabel('y - axis')
- });
+# giving a title to my graph
+plt.title('My first graph!')
-});
+# function to show the plot
+plt.show()`;
+
+// modified snippet from https://www.geeksforgeeks.org/python-pandas-dataframe/
+const savePlot = `import pandas as pd
+import matplotlib.pyplot as plt
+data_dict = {'name': ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'],
+ 'age': [20, 20, 21, 20, 21, 20],
+ 'math_marks': [100, 90, 91, 98, 92, 95],
+ 'physics_marks': [90, 100, 91, 92, 98, 95],
+ 'chem_marks': [93, 89, 99, 92, 94, 92]
+ }
+
+df = pd.DataFrame(data_dict)
+
+df.plot(kind='scatter',
+ x='math_marks',
+ y='physics_marks',
+ color='red')
+
+plt.title('ScatterPlot')
+plt.show()`;
+
+
+const bgplot = `import bqplot.pyplot as bplt
+import numpy as np
+
+x = np.linspace(-10, 10, 100)
+y = np.sin(x)
+axes_opts = {"x": {"label": "X"}, "y": {"label": "Y"}}
+
+fig = bplt.figure(title="Line Chart")
+line = bplt.plot(
+ x=x, y=y, axes_options=axes_opts
+)
+
+bplt.show()`;
+
+const ipydatagrid = `import pandas as pd
+from ipydatagrid import DataGrid
+data= pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, index=["One", "Two", "Three"])
+DataGrid(data)
+DataGrid(data, selection_mode="cell", editable=True)`;
+
+const ipyleaflet = `from ipyleaflet import Map, Marker, display
+center = (52.204793, 360.121558)
+map = Map(center=center, zoom=12)
+
+# Add a draggable marker to the map
+# Dragging the marker updates the marker.location value in Python
+marker = Marker(location=center, draggable=True)
+map.add_control(marker)
+
+display(map)`;
+
+const plotly = `import hvplot.pandas
+import pandas as pd
+hvplot.extension('plotly')
+pd.DataFrame(dict(x=[1,2,3], y=[4,5,6])).hvplot.scatter(x="x", y="y")`;
+
+const ipytree = `from ipytree import Tree, Node
+tree = Tree(stripes=True)
+tree
+tree
+node1 = Node('node1')
+tree.add_node(node1)
+node2 = Node('node2')
+tree.add_node(node2)
+tree.nodes = [node2, node1]
+node3 = Node('node3', disabled=True)
+node4 = Node('node4')
+node5 = Node('node5', [Node('1'), Node('2')])
+node2.add_node(node3)
+node2.add_node(node4)
+node2.add_node(node5)
+tree.add_node(Node('node6'), 1)
+node2.add_node(Node('node7'), 2)
+
+tree`;
+
+const ipywidgetOutput = `import ipywidgets
+output = ipywidgets.Output()
+output`;
+
+const bokeh = `from bokeh.plotting import figure, output_file, show
+
+# instantiating the figure object
+graph = figure(title = "Bokeh Line Graph")
+
+# the points to be plotted
+x = [1, 2, 3, 4, 5]
+y = [5, 4, 3, 2, 1]
+
+# plotting the line graph
+graph.line(x, y)
+
+# displaying the model
+show(graph)`;
+
+const rBasicPlot = `cars <- c(1, 3, 6, 4, 9)
+plot(cars, type="o", col="blue")
+title(main="Autos", col.main="red", font.main=4)`;
+
+const rSavePlot = `cars <- c(1, 3, 6, 4, 9)
+plot(cars, type="o", col="blue")
+title(main="Autos", col.main="red", font.main=4)`;
+
+const rplot = `library('corrr')
+
+x <- correlate(mtcars)
+rplot(x)
+
+# Common use is following rearrange and shave
+x <- rearrange(x, absolute = FALSE)
+x <- shave(x)
+rplot(x)
+rplot(x, print_cor = TRUE)
+rplot(x, shape = 20, colors = c("red", "green"), legend = TRUE)`;
+
+const highcharter = `library(highcharter)
+
+data("mpg", "diamonds", "economics_long", package = "ggplot2")
+
+hchart(mpg, "point", hcaes(x = displ, y = cty, group = year))`;
+
+const leaflet = `library(leaflet)
+m = leaflet() %>% addTiles()
+m = m %>% setView(-93.65, 42.0285, zoom = 17)
+m %>% addPopups(-93.65, 42.0285, 'Here is the Department of Statistics, ISU')`;
+
+const rPlotly = `library(plotly)
+fig <- plot_ly(midwest, x = ~percollege, color = ~state, type = "box")
+fig`;
diff --git a/test/smoke/plots/pythonScatterplot.png b/test/smoke/src/areas/positron/plots/pythonScatterplot.png
similarity index 100%
rename from test/smoke/plots/pythonScatterplot.png
rename to test/smoke/src/areas/positron/plots/pythonScatterplot.png
diff --git a/test/smoke/src/areas/positron/quarto/quarto.test.ts b/test/smoke/src/areas/positron/quarto/quarto.test.ts
index 7db51dfa29a..a71bdebdd70 100644
--- a/test/smoke/src/areas/positron/quarto/quarto.test.ts
+++ b/test/smoke/src/areas/positron/quarto/quarto.test.ts
@@ -4,65 +4,70 @@
*--------------------------------------------------------------------------------------------*/
import { Application } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-import { expect } from '@playwright/test';
+import { test, expect } from '../_test.setup';
const path = require('path');
const fs = require('fs-extra');
+let isWeb = false;
-describe('Quarto #web', () => {
- setupAndStartApp();
- let app: Application;
+test.use({
+ suiteId: __filename
+});
- before(async function () {
- app = this.app as Application;
+test.describe('Quarto', { tag: ['@web'] }, () => {
+ test.beforeAll(async function ({ app, browserName }) {
await app.workbench.quickaccess.openFile(path.join(app.workspacePathOrFolder, 'workspaces', 'quarto_basic', 'quarto_basic.qmd'));
+ isWeb = browserName === 'chromium';
});
- afterEach(async function () {
+ test.afterEach(async function ({ app }) {
await deleteGeneratedFiles(app);
});
- it('should be able to render html [C842847]', async function () {
+ test('should be able to render html [C842847]', async function ({ app }) {
await renderQuartoDocument(app, 'html');
await verifyDocumentExists(app, 'html');
});
- it('should be able to render docx [C842848]', async function () {
+ test('should be able to render docx [C842848]', async function ({ app }) {
await renderQuartoDocument(app, 'docx');
await verifyDocumentExists(app, 'docx');
});
- it('should be able to render pdf (LaTeX) [C842890]', async function () {
+ test('should be able to render pdf (LaTeX) [C842890]', async function ({ app }) {
await renderQuartoDocument(app, 'pdf');
await verifyDocumentExists(app, 'pdf');
});
- it('should be able to render pdf (typst) [C842889]', async function () {
+ test('should be able to render pdf (typst) [C842889]', async function ({ app }) {
await renderQuartoDocument(app, 'typst');
await verifyDocumentExists(app, 'pdf');
});
- it('should be able to generate preview [C842891]', async function () {
+ test('should be able to generate preview [C842891]', async function ({ app }) {
await app.workbench.quickaccess.runCommand('quarto.preview', { keepOpen: true });
+ const viewerFrame = app.workbench.positronViewer.getViewerFrame().frameLocator('iframe');
- const previewHeader = app.workbench.positronViewer.getViewerFrame().frameLocator('iframe').locator('h1');
- await expect(previewHeader).toBeVisible({ timeout: 20000 });
- await expect(previewHeader).toHaveText('Diamond sizes');
+ // verify preview displays
+ expect(await viewerFrame.locator('h1').innerText()).toBe('Diamond sizes');
});
});
const renderQuartoDocument = async (app: Application, fileExtension: string) => {
- await app.workbench.quickaccess.runCommand('quarto.render.document', { keepOpen: true });
- await app.workbench.quickinput.selectQuickInputElementContaining(fileExtension);
+ await test.step(`render quarto document`, async () => {
+ await app.workbench.quickaccess.runCommand('quarto.render.document', { keepOpen: true });
+ await app.workbench.quickinput.selectQuickInputElementContaining(fileExtension);
+ });
};
const verifyDocumentExists = async (app: Application, fileExtension: string) => {
+ // there is a known issue with canvas interactions in webview
+ if (!isWeb) { await expect(app.code.driver.page.getByText(`Output created: quarto_basic.${fileExtension}`)).toBeVisible({ timeout: 30000 }); }
+
await expect(async () => {
- await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.includes(`Output created: quarto_basic.${fileExtension}`)));
expect(await fileExists(app, `quarto_basic.${fileExtension}`)).toBe(true);
- }).toPass();
+ }).toPass({ timeout: 15000 });
};
const deleteGeneratedFiles = async (app: Application) => {
diff --git a/test/smoke/src/areas/positron/rmarkdown/rmarkdown.test.ts b/test/smoke/src/areas/positron/r-markdown/r-markdown.test.ts
similarity index 72%
rename from test/smoke/src/areas/positron/rmarkdown/rmarkdown.test.ts
rename to test/smoke/src/areas/positron/r-markdown/r-markdown.test.ts
index 557c571ff7e..631351556c0 100644
--- a/test/smoke/src/areas/positron/rmarkdown/rmarkdown.test.ts
+++ b/test/smoke/src/areas/positron/r-markdown/r-markdown.test.ts
@@ -3,22 +3,15 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { Application, PositronRFixtures } from '../../../../../automation';
import { join } from 'path';
-import { expect } from '@playwright/test';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
+test.use({
+ suiteId: __filename
+});
-describe('RMarkdown #web', () => {
- setupAndStartApp();
-
- before(async function () {
- // Executes once before executing all tests.
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- it('Render RMarkdown [C680618]', async function () {
- const app = this.app as Application; //Get handle to application
+test.describe('R Markdown', { tag: ['@web'] }, () => {
+ test('Render R Markdown [C680618]', async function ({ app, r }) {
await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'workspaces', 'basic-rmd-file', 'basicRmd.rmd'));
// Sometimes running render too quickly fails, saying pandoc is not installed.
@@ -37,12 +30,7 @@ describe('RMarkdown #web', () => {
});
// test depends on the previous test
- it('Preview RMarkdown [C709147]', async function () {
-
- this.timeout(90000);
-
- const app = this.app as Application; //Get handle to application
-
+ test('Preview R Markdown [C709147]', async function ({ app, r }) {
// Preview
await app.code.dispatchKeybinding(process.platform === 'darwin' ? 'cmd+shift+k' : 'ctrl+shift+k');
diff --git a/test/smoke/src/areas/positron/r-pkg-development/r-pkg-development.test.ts b/test/smoke/src/areas/positron/r-pkg-development/r-pkg-development.test.ts
index ed7eca7dcb4..b2e18fb76bd 100644
--- a/test/smoke/src/areas/positron/r-pkg-development/r-pkg-development.test.ts
+++ b/test/smoke/src/areas/positron/r-pkg-development/r-pkg-development.test.ts
@@ -4,79 +4,74 @@
*--------------------------------------------------------------------------------------------*/
import path = require('path');
-import { Application, PositronRFixtures, PositronUserSettingsFixtures } from '../../../../../automation';
-import { expect } from '@playwright/test';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
+import { PositronUserSettingsFixtures } from '../../../../../automation';
-let logger;
+test.use({
+ suiteId: __filename
+});
-describe('R Package Development #web', () => {
- let app: Application;
- let userSettings: PositronUserSettingsFixtures;
- logger = setupAndStartApp();
- describe('R Package Development - R', () => {
- before(async function () {
- app = this.app as Application;
- try {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- userSettings = new PositronUserSettingsFixtures(app);
+test.describe('R Package Development', { tag: ['@web'] }, () => {
+ let userSettings: PositronUserSettingsFixtures;
- // don't use native file picker
- await userSettings.setUserSetting(['files.simpleDialog.enable', 'true']);
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- } catch (e) {
- this.app.code.driver.takeScreenshot('rPackageSetup');
- throw e;
- }
- });
+ test.beforeAll(async function ({ app, r }) {
+ try {
+ userSettings = new PositronUserSettingsFixtures(app);
- after(async function () {
- // unset the use of the VSCode file picker
- await userSettings.unsetUserSettings();
- });
+ // don't use native file picker
+ await userSettings.setUserSetting(['files.simpleDialog.enable', 'true']);
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ } catch (e) {
+ app.code.driver.takeScreenshot('rPackageSetup');
+ throw e;
+ }
+ });
- it('R Package Development Tasks [C809821]', async function () {
+ test.afterAll(async function () {
+ // unset the use of the VSCode file picker
+ await userSettings.unsetUserSettings();
+ });
- this.timeout(200000);
+ test('R Package Development Tasks [C809821]', async function ({ app, logger }) {
+ test.slow();
- await expect(async () => {
- // Navigate to https://github.com/posit-dev/qa-example-content/tree/main/workspaces/r_testing
- // This is an R package embedded in qa-example-content
- await app.workbench.quickaccess.runCommand('workbench.action.files.openFolder', { keepOpen: true });
- await app.workbench.quickinput.waitForQuickInputOpened();
- await app.workbench.quickinput.type(path.join(app.workspacePathOrFolder, 'workspaces', 'r_testing'));
- // Had to add a positron class, because Microsoft did not have this:
- await app.workbench.quickinput.clickOkOnQuickInput();
+ await expect(async () => {
+ // Navigate to https://github.com/posit-dev/qa-example-content/tree/main/workspaces/r_testing
+ // This is an R package embedded in qa-example-content
+ await app.workbench.quickaccess.runCommand('workbench.action.files.openFolder', { keepOpen: true });
+ await app.workbench.quickinput.waitForQuickInputOpened();
+ await app.workbench.quickinput.type(path.join(app.workspacePathOrFolder, 'workspaces', 'r_testing'));
+ // Had to add a positron class, because Microsoft did not have this:
+ await app.workbench.quickinput.clickOkOnQuickInput();
- // Wait for the console to be ready
- await app.workbench.positronConsole.waitForReady('>', 10000);
- }).toPass({ timeout: 70000 });
+ // Wait for the console to be ready
+ await app.workbench.positronConsole.waitForReady('>', 10000);
+ }).toPass({ timeout: 70000 });
- logger.log('Test R Package');
- await app.workbench.quickaccess.runCommand('r.packageTest');
- await expect(async () => {
- await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.startsWith('[ FAIL 1 | WARN 0 | SKIP 0 | PASS 16 ]')));
- await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('Terminal will be reused by tasks')));
- }).toPass({ timeout: 70000 });
+ logger.log('Test R Package');
+ await app.workbench.quickaccess.runCommand('r.packageTest');
+ await expect(async () => {
+ await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.startsWith('[ FAIL 1 | WARN 0 | SKIP 0 | PASS 16 ]')));
+ await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('Terminal will be reused by tasks')));
+ }).toPass({ timeout: 70000 });
- logger.log('Check R Package');
- await app.workbench.quickaccess.runCommand('workbench.action.terminal.clear');
- await app.workbench.quickaccess.runCommand('r.packageCheck');
- await expect(async () => {
- await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.startsWith('Error: R CMD check found ERRORs')));
- await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('Terminal will be reused by tasks')));
- }).toPass({ timeout: 70000 });
+ logger.log('Check R Package');
+ await app.workbench.quickaccess.runCommand('workbench.action.terminal.clear');
+ await app.workbench.quickaccess.runCommand('r.packageCheck');
+ await expect(async () => {
+ await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.startsWith('Error: R CMD check found ERRORs')));
+ await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.includes('Terminal will be reused by tasks')));
+ }).toPass({ timeout: 70000 });
- logger.log('Install R Package and Restart R');
- await app.workbench.quickaccess.runCommand('r.packageInstall');
- await expect(async () => {
- await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.startsWith('✔ Installed testfun 0.0.0.9000')));
- await app.workbench.positronConsole.waitForReady('>');
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('restarted')));
- }).toPass({ timeout: 70000 });
- });
+ logger.log('Install R Package and Restart R');
+ await app.workbench.quickaccess.runCommand('r.packageInstall');
+ await expect(async () => {
+ await app.workbench.terminal.waitForTerminalText(buffer => buffer.some(line => line.startsWith('✔ Installed testfun 0.0.0.9000')));
+ await app.workbench.positronConsole.waitForReady('>');
+ await expect(app.workbench.positronConsole.activeConsole.getByText('restarted')).toBeVisible({ timeout: 30000 });
+ }).toPass({ timeout: 70000 });
});
});
diff --git a/test/smoke/src/areas/positron/reticulate/reticulate.test.ts b/test/smoke/src/areas/positron/reticulate/reticulate.test.ts
index b5818ca4ee4..fb81f736e07 100644
--- a/test/smoke/src/areas/positron/reticulate/reticulate.test.ts
+++ b/test/smoke/src/areas/positron/reticulate/reticulate.test.ts
@@ -3,81 +3,74 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { expect } from '@playwright/test';
-import { Application, PositronRFixtures, PositronUserSettingsFixtures, UserSetting } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
+import { PositronUserSettingsFixtures, UserSetting } from '../../../../../automation';
+
+test.use({
+ suiteId: __filename
+});
// In order to run this test on Windows, I think we need to set the env var:
// RETICULATE_PYTHON
// to the installed python path
-describe('Reticulate #web', () => {
- setupAndStartApp();
- let app: Application;
- let userSettings: PositronUserSettingsFixtures;
-
- describe('Reticulate', () => {
- before(async function () {
- app = this.app as Application;
-
- try {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- userSettings = new PositronUserSettingsFixtures(app);
-
- // remove this once https://github.com/posit-dev/positron/issues/5226
- // is resolved
- const kernelSupervisorSetting: UserSetting = ['positronKernelSupervisor.enable', 'false'];
- const reticulateSetting: UserSetting = ['positron.reticulate.enabled', 'true'];
-
- await userSettings.setUserSettings([
- kernelSupervisorSetting,
- reticulateSetting
- ]
- );
-
- } catch (e) {
- this.app.code.driver.takeScreenshot('reticulateSetup');
- throw e;
- }
- });
-
- after(async function () {
- await userSettings.unsetUserSettings();
-
- });
-
- it('R - Verify Basic Reticulate Functionality [C...]', async function () {
+let userSettings: PositronUserSettingsFixtures;
+
+test.describe('Reticulate', {
+ tag: ['@web'],
+ annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/5226' }]
+}, () => {
+ test.beforeAll(async function ({ app }) {
+ try {
+ userSettings = new PositronUserSettingsFixtures(app);
+
+ // remove this once https://github.com/posit-dev/positron/issues/5226
+ // is resolved
+ const kernelSupervisorSetting: UserSetting = ['positronKernelSupervisor.enable', 'false'];
+ const reticulateSetting: UserSetting = ['positron.reticulate.enabled', 'true'];
+
+ await userSettings.setUserSettings([
+ kernelSupervisorSetting,
+ reticulateSetting
+ ]);
+
+ } catch (e) {
+ app.code.driver.takeScreenshot('reticulateSetup');
+ throw e;
+ }
+ });
- await app.workbench.positronConsole.pasteCodeToConsole('reticulate::repl_python()');
- await app.workbench.positronConsole.sendEnterKey();
+ test.afterAll(async function () {
+ await userSettings.unsetUserSettings();
- try {
- await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Yes/no/cancel')));
- await app.workbench.positronConsole.typeToConsole('no');
- await app.workbench.positronConsole.sendEnterKey();
+ });
- } catch {
- // Prompt did not appear
- }
+ test('R - Verify Basic Reticulate Functionality [C...]', async function ({ app, r, interpreter }) {
- await app.workbench.positronConsole.waitForReady('>>>');
+ await app.workbench.positronConsole.pasteCodeToConsole('reticulate::repl_python()');
+ await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronConsole.pasteCodeToConsole('x=100');
+ try {
+ await app.workbench.positronConsole.waitForConsoleContents((contents) => contents.some((line) => line.includes('Yes/no/cancel')));
+ await app.workbench.positronConsole.typeToConsole('no');
await app.workbench.positronConsole.sendEnterKey();
+ } catch {
+ // Prompt did not appear
+ }
- await PositronRFixtures.SetupFixtures(this.app as Application);
+ await app.workbench.positronConsole.waitForReady('>>>');
+ await app.workbench.positronConsole.pasteCodeToConsole('x=100');
+ await app.workbench.positronConsole.sendEnterKey();
- await app.workbench.positronConsole.pasteCodeToConsole('y<-reticulate::py$x');
- await app.workbench.positronConsole.sendEnterKey();
+ await interpreter.set('R');
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ await app.workbench.positronConsole.pasteCodeToConsole('y<-reticulate::py$x');
+ await app.workbench.positronConsole.sendEnterKey();
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
- await expect(async () => {
- const variablesMap = await app.workbench.positronVariables.getFlatVariables();
- expect(variablesMap.get('y')).toStrictEqual({ value: '100', type: 'int' });
- }).toPass({ timeout: 60000 });
+ await expect(async () => {
+ const variablesMap = await app.workbench.positronVariables.getFlatVariables();
+ expect(variablesMap.get('y')).toStrictEqual({ value: '100', type: 'int' });
+ }).toPass({ timeout: 60000 });
- });
});
});
diff --git a/test/smoke/src/areas/positron/test-explorer/test-explorer.test.ts b/test/smoke/src/areas/positron/test-explorer/test-explorer.test.ts
index 25cb9fd7a1d..50820055cd4 100644
--- a/test/smoke/src/areas/positron/test-explorer/test-explorer.test.ts
+++ b/test/smoke/src/areas/positron/test-explorer/test-explorer.test.ts
@@ -4,117 +4,106 @@
*--------------------------------------------------------------------------------------------*/
import path = require('path');
-import { Application, PositronRFixtures, PositronUserSettingsFixtures } from '../../../../../automation';
-import { expect } from '@playwright/test';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
+import { PositronUserSettingsFixtures } from '../../../../../automation';
-describe('Test Explorer', () => {
- setupAndStartApp();
- let app: Application;
- let userSettings: PositronUserSettingsFixtures;
-
- describe('Test Explorer - R', () => {
- before(async function () {
- app = this.app as Application;
-
- try {
-
- await PositronRFixtures.SetupFixtures(this.app as Application);
-
- userSettings = new PositronUserSettingsFixtures(app);
-
- // don't use native file picker
- await userSettings.setUserSetting([
- 'files.simpleDialog.enable',
- 'true',
- ]);
-
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
- await app.workbench.positronConsole.barClearButton.click();
- await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
-
- } catch (e) {
- this.app.code.driver.takeScreenshot('testExplorerSetup');
- throw e;
- }
- });
-
- after(async function () {
-
- // unset the use of the VSCode file picker
- await userSettings.unsetUserSettings();
+test.use({
+ suiteId: __filename
+});
- });
+test.describe('Test Explorer', () => {
+ let userSettings: PositronUserSettingsFixtures;
- it('R - Verify Basic Test Explorer Functionality [C749378]', async function () {
+ test.beforeAll(async function ({ app, r }) {
+ try {
+ userSettings = new PositronUserSettingsFixtures(app);
+
+ // don't use native file picker
+ await userSettings.setUserSetting([
+ 'files.simpleDialog.enable',
+ 'true',
+ ]);
+
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ await app.workbench.positronConsole.barClearButton.click();
+ await app.workbench.quickaccess.runCommand('workbench.action.toggleAuxiliaryBar');
+ } catch (e) {
+ app.code.driver.takeScreenshot('testExplorerSetup');
+ throw e;
+ }
+ });
- await expect(async () => {
- // Navigate to https://github.com/posit-dev/qa-example-content/tree/main/workspaces/r_testing
- // This is an R package embedded in qa-example-content
- await app.workbench.quickaccess.runCommand('workbench.action.files.openFolder', { keepOpen: true });
- await app.workbench.quickinput.waitForQuickInputOpened();
- await app.workbench.quickinput.type(path.join(app.workspacePathOrFolder, 'workspaces', 'r_testing'));
- // Had to add a positron class, because Microsoft did not have this:
- await app.workbench.quickinput.clickOkOnQuickInput();
+ test.afterEach(async function () {
+ // unset the use of the VSCode file picker
+ await userSettings.unsetUserSettings();
+ });
- // Wait for the console to be ready
- await app.workbench.positronConsole.waitForReady('>', 10000);
- }).toPass({ timeout: 50000 });
+ test('R - Verify Basic Test Explorer Functionality [C749378]', async function ({ app }) {
+ await expect(async () => {
+ // Navigate to https://github.com/posit-dev/qa-example-content/tree/main/workspaces/r_testing
+ // This is an R package embedded in qa-example-content
+ await app.workbench.quickaccess.runCommand('workbench.action.files.openFolder', { keepOpen: true });
+ await app.workbench.quickinput.waitForQuickInputOpened();
+ await app.workbench.quickinput.type(path.join(app.workspacePathOrFolder, 'workspaces', 'r_testing'));
+ // Had to add a positron class, because Microsoft did not have this:
+ await app.workbench.quickinput.clickOkOnQuickInput();
- await expect(async () => {
- await app.workbench.positronTestExplorer.clickTestExplorerIcon();
+ // Wait for the console to be ready
+ await app.workbench.positronConsole.waitForReady('>', 10000);
+ }).toPass({ timeout: 50000 });
- const projectFiles = await app.workbench.positronTestExplorer.getTestExplorerFiles();
+ await expect(async () => {
+ await app.workbench.positronTestExplorer.clickTestExplorerIcon();
- // test-mathstuff.R is the last section of tests in https://github.com/posit-dev/qa-example-content/tree/main/workspaces/r_testing
- expect(projectFiles).toContain('test-mathstuff.R');
- }).toPass({ timeout: 50000 });
+ const projectFiles = await app.workbench.positronTestExplorer.getTestExplorerFiles();
- await app.workbench.positronTestExplorer.runAllTests();
+ // test-mathstuff.R is the last section of tests in https://github.com/posit-dev/qa-example-content/tree/main/workspaces/r_testing
+ expect(projectFiles).toContain('test-mathstuff.R');
+ }).toPass({ timeout: 50000 });
- await expect(async () => {
- const testResults = await app.workbench.positronTestExplorer.getTestResults();
+ await app.workbench.positronTestExplorer.runAllTests();
- expect(testResults[0].caseText).toBe('nothing really');
- expect(testResults[0].status).toBe('fail');
+ await expect(async () => {
+ const testResults = await app.workbench.positronTestExplorer.getTestResults();
- expect(testResults[1].caseText).toBe('subtraction works');
- expect(testResults[1].status).toBe('pass');
+ expect(testResults[0].caseText).toBe('nothing really');
+ expect(testResults[0].status).toBe('fail');
- expect(testResults[2].caseText).toBe('subtraction `still` "works"');
- expect(testResults[2].status).toBe('pass');
+ expect(testResults[1].caseText).toBe('subtraction works');
+ expect(testResults[1].status).toBe('pass');
- expect(testResults[3].caseText).toBe('x is \'a\'');
- expect(testResults[3].status).toBe('pass');
+ expect(testResults[2].caseText).toBe('subtraction `still` "works"');
+ expect(testResults[2].status).toBe('pass');
- expect(testResults[4].caseText).toBe('x is \'a\' AND y is \'b\'');
- expect(testResults[4].status).toBe('pass');
+ expect(testResults[3].caseText).toBe('x is \'a\'');
+ expect(testResults[3].status).toBe('pass');
- expect(testResults[5].caseText).toBe('whatever');
- expect(testResults[5].status).toBe('pass');
+ expect(testResults[4].caseText).toBe('x is \'a\' AND y is \'b\'');
+ expect(testResults[4].status).toBe('pass');
- expect(testResults[6].caseText).toBe('can \'add\' two numbers');
- expect(testResults[6].status).toBe('pass');
+ expect(testResults[5].caseText).toBe('whatever');
+ expect(testResults[5].status).toBe('pass');
- expect(testResults[7].caseText).toBe('can multiply two numbers');
- expect(testResults[7].status).toBe('pass');
+ expect(testResults[6].caseText).toBe('can \'add\' two numbers');
+ expect(testResults[6].status).toBe('pass');
- expect(testResults[8].caseText).toBe('can be multiplied by a scalar');
- expect(testResults[8].status).toBe('pass');
+ expect(testResults[7].caseText).toBe('can multiply two numbers');
+ expect(testResults[7].status).toBe('pass');
- expect(testResults[9].caseText).toBe('is true');
- expect(testResults[9].status).toBe('pass');
+ expect(testResults[8].caseText).toBe('can be multiplied by a scalar');
+ expect(testResults[8].status).toBe('pass');
- expect(testResults[10].caseText).toBe('can add two numbers');
- expect(testResults[10].status).toBe('pass');
+ expect(testResults[9].caseText).toBe('is true');
+ expect(testResults[9].status).toBe('pass');
- expect(testResults[11].caseText).toBe('can multiply two numbers');
- expect(testResults[11].status).toBe('pass');
+ expect(testResults[10].caseText).toBe('can add two numbers');
+ expect(testResults[10].status).toBe('pass');
- expect(testResults[12].caseText).toBe('a second it()');
- expect(testResults[12].status).toBe('pass');
- }).toPass({ timeout: 50000 });
+ expect(testResults[11].caseText).toBe('can multiply two numbers');
+ expect(testResults[11].status).toBe('pass');
- });
+ expect(testResults[12].caseText).toBe('a second it()');
+ expect(testResults[12].status).toBe('pass');
+ }).toPass({ timeout: 50000 });
});
});
diff --git a/test/smoke/src/areas/positron/top-action-bar/interpreter-dropdown.test.ts b/test/smoke/src/areas/positron/top-action-bar/interpreter-dropdown.test.ts
index f996c8d3cf6..cef7e0a370e 100644
--- a/test/smoke/src/areas/positron/top-action-bar/interpreter-dropdown.test.ts
+++ b/test/smoke/src/areas/positron/top-action-bar/interpreter-dropdown.test.ts
@@ -3,77 +3,30 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { expect } from '@playwright/test';
import {
- Application,
PositronConsole,
PositronInterpreterDropdown,
- PositronUserSettingsFixtures,
- QuickAccess,
} from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
-describe.skip('Interpreter Dropdown in Top Action Bar #web', () => {
- setupAndStartApp();
- let app: Application;
+test.use({
+ suiteId: __filename
+});
+
+test.describe.skip('Interpreter Dropdown in Top Action Bar', { tag: ['@web'] }, () => {
let interpreterDropdown: PositronInterpreterDropdown;
let positronConsole: PositronConsole;
- let quickaccess: QuickAccess;
- let userSettings: PositronUserSettingsFixtures;
- let desiredPython: string;
- let desiredR: string;
- before(async function () {
- app = this.app as Application;
+ test.beforeAll(async function ({ app }) {
interpreterDropdown = app.workbench.positronInterpreterDropdown;
positronConsole = app.workbench.positronConsole;
- quickaccess = app.workbench.quickaccess;
- userSettings = new PositronUserSettingsFixtures(app);
- desiredPython = process.env.POSITRON_PY_VER_SEL!;
- desiredR = process.env.POSITRON_R_VER_SEL!;
+ });
- /**
- * Ensure that no interpreters are running before starting the tests. This is necessary
- * because interactions with the interpreter dropdown to select an interpreter can get
- * bulldozed by automatic interpreter startup.
- */
+ test('Python interpreter starts and shows running [C707212]', async function ({ app }) {
+ const desiredPython = process.env.POSITRON_PY_VER_SEL!;
- // Wait for the console to be ready
- await positronConsole.waitForReadyOrNoInterpreter();
-
- try {
- // If no interpreters are running, we're good to go
- await positronConsole.waitForNoInterpretersRunning(50);
- return;
- } catch (error) {
- try {
- // If an interpreter is running, we'll shut it down and disable automatic startup
-
- // Shutdown running interpreter
- await quickaccess.runCommand('workbench.action.languageRuntime.shutdown');
- await positronConsole.waitForInterpreterShutdown();
-
- // Disable automatic startup of interpreters in user settings. This setting will be
- // cleared in the next app startup for a subsequent test, so we don't need to unset
- // it.
- await userSettings.setUserSetting([
- 'positron.interpreters.automaticStartup',
- 'false',
- ]);
-
- // Reload the window
- // keepOpen is set to true so we don't need to wait for the prompt input to close
- // (it will never close since the app gets reloaded)
- await quickaccess.runCommand('workbench.action.reloadWindow', { keepOpen: true });
- } catch (e) {
- this.app.code.driver.takeScreenshot('interpreterDropdownSetup');
- throw e;
- }
- }
- });
- it('Python interpreter starts and shows running [C707212]', async function () {
// Start a Python interpreter using the interpreter dropdown
await expect(
async () =>
@@ -81,8 +34,8 @@ describe.skip('Interpreter Dropdown in Top Action Bar #web', () => {
).toPass({ timeout: 30_000 });
// Install ipykernel if prompted
- if (await this.app.workbench.positronPopups.popupCurrentlyOpen()) {
- await this.app.workbench.positronPopups.installIPyKernel();
+ if (await app.workbench.positronPopups.popupCurrentlyOpen()) {
+ await app.workbench.positronPopups.installIPyKernel();
}
// Wait for the console to be ready
@@ -108,9 +61,7 @@ describe.skip('Interpreter Dropdown in Top Action Bar #web', () => {
await interpreterDropdown.closeInterpreterDropdown();
});
- it('Python interpreter restarts and shows running [C707213]', async function () {
- // NOTE: This test is dependent on 'Python interpreter starts and shows running' having run successfully
-
+ test('Python interpreter restarts and shows running [C707213]', async function ({ python }) {
// Restart the active Python interpreter
await interpreterDropdown.restartPrimaryInterpreter('Python');
@@ -139,7 +90,9 @@ describe.skip('Interpreter Dropdown in Top Action Bar #web', () => {
await interpreterDropdown.closeInterpreterDropdown();
});
- it('R interpreter starts and shows running [C707214]', async function () {
+ test('R interpreter starts and shows running [C707214]', async function () {
+ const desiredR = process.env.POSITRON_R_VER_SEL!;
+
// Start an R interpreter using the interpreter dropdown
await expect(
async () => await interpreterDropdown.selectInterpreter('R', desiredR)
@@ -174,9 +127,7 @@ describe.skip('Interpreter Dropdown in Top Action Bar #web', () => {
await interpreterDropdown.closeInterpreterDropdown();
});
- it('R interpreter stops and shows inactive [C707215]', async function () {
- // NOTE: This test is dependent on 'R interpreter starts and shows running' having run successfully
-
+ test('R interpreter stops and shows inactive [C707215]', async function ({ r }) {
// Stop the active R interpreter
expect(async () => {
await interpreterDropdown.stopPrimaryInterpreter('R');
diff --git a/test/smoke/src/areas/positron/top-action-bar/top-action-bar-save.test.ts b/test/smoke/src/areas/positron/top-action-bar/top-action-bar-save.test.ts
new file mode 100644
index 00000000000..6aa912a07b2
--- /dev/null
+++ b/test/smoke/src/areas/positron/top-action-bar/top-action-bar-save.test.ts
@@ -0,0 +1,110 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
+ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { join } from 'path';
+import { test, expect } from '../_test.setup';
+import { PositronUserSettingsFixtures } from '../../../../../automation';
+
+test.use({
+ suiteId: __filename
+});
+
+test.describe('Top Action Bar - Save Actions', {
+ tag: ['@web']
+}, () => {
+
+ let userSettings: PositronUserSettingsFixtures;
+
+ test.beforeAll(async function ({ app }) {
+ if (app.web) {
+ userSettings = new PositronUserSettingsFixtures(app);
+ await userSettings.setUserSetting(['files.autoSave', 'false']);
+ }
+ });
+
+ test('Save and Save All both disabled when no unsaved editors are open [C656253]', async function ({ app }) {
+ await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
+ await expect(async () => {
+ expect(await app.workbench.positronTopActionBar.saveButton.isDisabled()).toBeTruthy();
+ expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy();
+ }).toPass({ timeout: 20000 });
+ });
+
+ test('Save enabled and Save All disabled when a single unsaved file is open [C656254]', async function ({ app }) {
+ const fileName = 'README.md';
+ await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName));
+ await app.workbench.quickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false });
+ await app.workbench.editors.selectTab(fileName);
+ await app.workbench.editor.waitForTypeInEditor(fileName, 'Puppies frolicking in a meadow of wildflowers');
+ // The file is now "dirty" and the save buttons should be enabled
+ await app.workbench.editors.waitForTab(fileName, true);
+ await expect(async () => {
+ expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
+ expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy();
+ }).toPass({ timeout: 10000 });
+ await app.workbench.positronTopActionBar.saveButton.click();
+ // The file is now saved, so the file should no longer be "dirty"
+ await app.workbench.editors.waitForTab(fileName, false);
+ await expect(async () => {
+ // The Save button stays enabled even when the active file is not "dirty"
+ expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
+ // The Save All button is disabled when less than 2 files are "dirty"
+ expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy();
+ }).toPass({ timeout: 10000 });
+ });
+
+ test('Save and Save All both enabled when multiple unsaved files are open [C656255]', async function ({ app }) {
+ const fileName1 = 'README.md';
+ const fileName2 = 'DESCRIPTION';
+ const text = 'Kittens playing with yarn';
+ // Open two files and type in some text
+ await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName1));
+ await app.workbench.quickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false });
+ await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName2));
+ await app.workbench.quickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false });
+ await app.workbench.editors.selectTab(fileName1);
+ await app.workbench.editor.waitForTypeInEditor(fileName1, text);
+ await app.workbench.editors.selectTab(fileName2);
+ await app.workbench.editor.waitForTypeInEditor(fileName2, text);
+ // The files are now "dirty" and the save buttons should be enabled
+ await app.workbench.editors.waitForTab(fileName1, true);
+ await app.workbench.editors.waitForTab(fileName2, true);
+ await expect(async () => {
+ expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
+ expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy();
+ }).toPass({ timeout: 10000 });
+ await app.workbench.positronTopActionBar.saveAllButton.click();
+ // The files are now saved, so the files should no longer be "dirty"
+ await app.workbench.editors.waitForTab(fileName1, false);
+ await app.workbench.editors.waitForTab(fileName2, false);
+ await expect(async () => {
+ // The Save button stays enabled even when the active file is not "dirty"
+ expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
+ // The Save All button is disabled when less than 2 files are "dirty"
+ expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy();
+ }).toPass({ timeout: 10000 });
+ });
+
+ test('Save and Save All both enabled when an unsaved new file is open [C656256]', async function ({ app }) {
+ const fileName = 'Untitled-1';
+ const text = 'Bunnies hopping through a field of clover';
+ // Open a new file and type in some text
+ await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
+ await app.workbench.quickaccess.runCommand('workbench.action.files.newUntitledFile', { keepOpen: false });
+ await app.workbench.editors.selectTab(fileName);
+ await app.workbench.editor.waitForTypeInEditor(fileName, text);
+ // The file is now "dirty" and the save buttons should be enabled
+ await app.workbench.editors.waitForTab(fileName, true);
+ await expect(async () => {
+ expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
+ expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy();
+ }).toPass({ timeout: 10000 });
+ // We won't try to click the Save buttons because a system dialog will pop up and we
+ // can't automate interactions with the native file dialog
+ });
+});
+
diff --git a/test/smoke/src/areas/positron/top-action-bar/top-action-bar.test.ts b/test/smoke/src/areas/positron/top-action-bar/top-action-bar.test.ts
deleted file mode 100644
index f2fa0785104..00000000000
--- a/test/smoke/src/areas/positron/top-action-bar/top-action-bar.test.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { join } from 'path';
-import { expect } from '@playwright/test';
-import { Application, PositronUserSettingsFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Top Action Bar #web', () => {
- setupAndStartApp();
-
- let userSettings: PositronUserSettingsFixtures;
-
- describe('Save Actions', () => {
- before(async function () {
- if (this.app.web) {
- userSettings = new PositronUserSettingsFixtures(this.app);
- await userSettings.setUserSetting(['files.autoSave', 'false']);
- }
- });
-
- after(async function () {
- if (this.app.web) {
- await userSettings.unsetUserSettings();
- }
- });
-
- it('Save and Save All both disabled when no unsaved editors are open [C656253]', async function () {
- const app = this.app as Application;
- await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
- await expect(async () => {
- expect(await app.workbench.positronTopActionBar.saveButton.isDisabled()).toBeTruthy();
- expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy();
- }).toPass({ timeout: 20000 });
- });
-
- it('Save enabled and Save All disabled when a single unsaved file is open [C656254]', async function () {
- const app = this.app as Application;
- const fileName = 'README.md';
- await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName));
- await app.workbench.quickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false });
- await app.workbench.editors.selectTab(fileName);
- await app.workbench.editor.waitForTypeInEditor(fileName, 'Puppies frolicking in a meadow of wildflowers');
- // The file is now "dirty" and the save buttons should be enabled
- await app.workbench.editors.waitForTab(fileName, true);
- await expect(async () => {
- expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
- expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy();
- }).toPass({ timeout: 10000 });
- await app.workbench.positronTopActionBar.saveButton.click();
- // The file is now saved, so the file should no longer be "dirty"
- await app.workbench.editors.waitForTab(fileName, false);
- await expect(async () => {
- // The Save button stays enabled even when the active file is not "dirty"
- expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
- // The Save All button is disabled when less than 2 files are "dirty"
- expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy();
- }).toPass({ timeout: 10000 });
- });
-
- it('Save and Save All both enabled when multiple unsaved files are open [C656255]', async function () {
- const app = this.app as Application;
- const fileName1 = 'README.md';
- const fileName2 = 'DESCRIPTION';
- const text = 'Kittens playing with yarn';
- // Open two files and type in some text
- await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName1));
- await app.workbench.quickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false });
- await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, fileName2));
- await app.workbench.quickaccess.runCommand('workbench.action.keepEditor', { keepOpen: false });
- await app.workbench.editors.selectTab(fileName1);
- await app.workbench.editor.waitForTypeInEditor(fileName1, text);
- await app.workbench.editors.selectTab(fileName2);
- await app.workbench.editor.waitForTypeInEditor(fileName2, text);
- // The files are now "dirty" and the save buttons should be enabled
- await app.workbench.editors.waitForTab(fileName1, true);
- await app.workbench.editors.waitForTab(fileName2, true);
- await expect(async () => {
- expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
- expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy();
- }).toPass({ timeout: 10000 });
- await app.workbench.positronTopActionBar.saveAllButton.click();
- // The files are now saved, so the files should no longer be "dirty"
- await app.workbench.editors.waitForTab(fileName1, false);
- await app.workbench.editors.waitForTab(fileName2, false);
- await expect(async () => {
- // The Save button stays enabled even when the active file is not "dirty"
- expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
- // The Save All button is disabled when less than 2 files are "dirty"
- expect(await app.workbench.positronTopActionBar.saveAllButton.isDisabled()).toBeTruthy();
- }).toPass({ timeout: 10000 });
- });
-
- it('Save and Save All both enabled when an unsaved new file is open [C656256]', async function () {
- const app = this.app as Application;
- const fileName = 'Untitled-1';
- const text = 'Bunnies hopping through a field of clover';
- // Open a new file and type in some text
- await app.workbench.quickaccess.runCommand('workbench.action.closeAllEditors', { keepOpen: false });
- await app.workbench.quickaccess.runCommand('workbench.action.files.newUntitledFile', { keepOpen: false });
- await app.workbench.editors.selectTab(fileName);
- await app.workbench.editor.waitForTypeInEditor(fileName, text);
- // The file is now "dirty" and the save buttons should be enabled
- await app.workbench.editors.waitForTab(fileName, true);
- await expect(async () => {
- expect(await app.workbench.positronTopActionBar.saveButton.isEnabled()).toBeTruthy();
- expect(await app.workbench.positronTopActionBar.saveAllButton.isEnabled()).toBeTruthy();
- }).toPass({ timeout: 10000 });
- // We won't try to click the Save buttons because a system dialog will pop up and we
- // can't automate interactions with the native file dialog
- });
- });
-
-});
-
diff --git a/test/smoke/src/areas/positron/variables/variables-expanded.test.ts b/test/smoke/src/areas/positron/variables/variables-expanded.test.ts
index c077079930f..14536309336 100644
--- a/test/smoke/src/areas/positron/variables/variables-expanded.test.ts
+++ b/test/smoke/src/areas/positron/variables/variables-expanded.test.ts
@@ -3,23 +3,19 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
-describe('Variables - Expanded View #web', () => {
- setupAndStartApp();
+test.use({
+ suiteId: __filename
+});
- beforeEach(async function () {
- const app = this.app as Application;
- await PositronPythonFixtures.SetupFixtures(app);
+test.describe('Variables - Expanded View', { tag: ['@web'] }, () => {
+ test.beforeEach(async function ({ app, python }) {
await app.workbench.positronConsole.executeCode('Python', script, '>>>');
await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
-
});
- it('Python - should display children values and types when variable is expanded', async function () {
- const app = this.app as Application;
+ test('Python - should display children values and types when variable is expanded', async function ({ app }) {
const variables = app.workbench.positronVariables;
await variables.expandVariable('df');
diff --git a/test/smoke/src/areas/positron/variables/variables-notebook.test.ts b/test/smoke/src/areas/positron/variables/variables-notebook.test.ts
index 97ea653c478..d160d256695 100644
--- a/test/smoke/src/areas/positron/variables/variables-notebook.test.ts
+++ b/test/smoke/src/areas/positron/variables/variables-notebook.test.ts
@@ -3,77 +3,62 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
-describe('Variables Pane - Notebook', () => {
-
- describe('Python Notebook Variables Pane #pr #web', () => {
- setupAndStartApp();
-
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
+test.use({
+ suiteId: __filename
+});
- it('Verifies Variables pane basic function for notebook with python interpreter [C669188]', async function () {
- const app = this.app as Application;
+test.afterEach(async function ({ app }) {
+ await app.workbench.positronNotebooks.closeNotebookWithoutSaving();
+ await app.workbench.positronLayouts.enterLayout('stacked');
+});
- await app.workbench.positronNotebooks.createNewNotebook();
+test.describe('Variables Pane - Notebook', { tag: ['@pr', '@web'] }, () => {
+ test('Python - Verifies Variables pane basic function for notebook [C669188]', async function ({ app, python }) {
+ await app.workbench.positronNotebooks.createNewNotebook();
- // workaround issue where starting multiple interpreters in quick succession can cause startup failure
- await app.code.wait(1000);
+ // workaround issue where starting multiple interpreters in quick succession can cause startup failure
+ await app.code.wait(1000);
- await app.workbench.positronNotebooks.selectInterpreter('Python Environments', process.env.POSITRON_PY_VER_SEL!);
- await app.workbench.positronNotebooks.addCodeToFirstCell('y = [2, 3, 4, 5]');
- await app.workbench.positronNotebooks.executeCodeInCell();
+ await app.workbench.positronNotebooks.selectInterpreter('Python Environments', process.env.POSITRON_PY_VER_SEL!);
+ await app.workbench.positronNotebooks.addCodeToFirstCell('y = [2, 3, 4, 5]');
+ await app.workbench.positronNotebooks.executeCodeInCell();
- const filename = 'Untitled-1.ipynb';
+ const filename = 'Untitled-1.ipynb';
- // temporary workaround for fact that variables group
- // not properly autoselected on web
- if (app.web) {
- await app.workbench.positronVariables.selectVariablesGroup(filename);
- }
+ // temporary workaround for fact that variables group
+ // not properly autoselected on web
+ if (app.web) {
+ await app.workbench.positronVariables.selectVariablesGroup(filename);
+ }
- const interpreter = app.workbench.positronVariables.interpreterLocator;
- await expect(interpreter).toBeVisible();
- await expect(interpreter).toHaveText(filename);
+ const interpreter = app.workbench.positronVariables.interpreterLocator;
+ await expect(interpreter).toBeVisible();
+ await expect(interpreter).toHaveText(filename);
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
- const variablesMap = await app.workbench.positronVariables.getFlatVariables();
- expect(variablesMap.get('y')).toStrictEqual({ value: '[2, 3, 4, 5]', type: 'list [4]' });
- });
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ const variablesMap = await app.workbench.positronVariables.getFlatVariables();
+ expect(variablesMap.get('y')).toStrictEqual({ value: '[2, 3, 4, 5]', type: 'list [4]' });
});
- describe('R Notebook Variables Pane #pr #web', () => {
- setupAndStartApp();
+ test('R - Verifies Variables pane basic function for notebook [C669189]', async function ({ app, r }) {
+ await app.workbench.positronNotebooks.createNewNotebook();
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
+ await app.workbench.positronNotebooks.selectInterpreter('R Environments', process.env.POSITRON_R_VER_SEL!);
+ await app.workbench.positronNotebooks.addCodeToFirstCell('y <- c(2, 3, 4, 5)');
+ await app.workbench.positronNotebooks.executeCodeInCell();
- it('Verifies Variables pane basic function for notebook with R interpreter [C669189]', async function () {
- const app = this.app as Application;
+ const interpreter = app.workbench.positronVariables.interpreterLocator;
+ await expect(interpreter).toBeVisible();
+ await expect(interpreter).toHaveText('Untitled-1.ipynb');
- await app.workbench.positronNotebooks.createNewNotebook();
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
- // workaround issue where starting multiple interpreters in quick succession can cause startup failure
- await app.code.wait(1000);
-
- await app.workbench.positronNotebooks.selectInterpreter('R Environments', process.env.POSITRON_R_VER_SEL!);
- await app.workbench.positronNotebooks.addCodeToFirstCell('y <- c(2, 3, 4, 5)');
- await app.workbench.positronNotebooks.executeCodeInCell();
-
- const interpreter = app.workbench.positronVariables.interpreterLocator;
- await expect(interpreter).toBeVisible();
- await expect(interpreter).toHaveText('Untitled-1.ipynb');
-
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ await expect(async () => {
const variablesMap = await app.workbench.positronVariables.getFlatVariables();
expect(variablesMap.get('y')).toStrictEqual({ value: '2 3 4 5', type: 'dbl [4]' });
- });
-
+ }).toPass({ timeout: 60000 });
});
});
diff --git a/test/smoke/src/areas/positron/variables/variables-pane.test.ts b/test/smoke/src/areas/positron/variables/variables-pane.test.ts
index a05d090d4e7..c5c9a92f1b2 100644
--- a/test/smoke/src/areas/positron/variables/variables-pane.test.ts
+++ b/test/smoke/src/areas/positron/variables/variables-pane.test.ts
@@ -3,88 +3,53 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test, expect } from '../_test.setup';
-describe('Variables Pane #web #win', () => {
- const logger = setupAndStartApp();
-
- describe('Python Variables Pane #web #pr', () => {
-
- before(async function () {
-
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
-
- });
-
- beforeEach(async function () {
- await this.app.workbench.positronLayouts.enterLayout('stacked');
-
- });
-
- it('Verifies Variables pane basic function with python interpreter [C628634]', async function () {
- const app = this.app as Application;
-
- const executeCode = async (code: string) => {
- await app.workbench.positronConsole.executeCode('Python', code, '>>>');
- };
-
- await executeCode('x=1');
- await executeCode('y=10');
- await executeCode('z=100');
-
- logger.log('Entered lines in console defining variables');
-
- await app.workbench.positronConsole.logConsoleContents();
-
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
-
- const variablesMap = await app.workbench.positronVariables.getFlatVariables();
-
- expect(variablesMap.get('x')).toStrictEqual({ value: '1', type: 'int' });
- expect(variablesMap.get('y')).toStrictEqual({ value: '10', type: 'int' });
- expect(variablesMap.get('z')).toStrictEqual({ value: '100', type: 'int' });
-
- });
+test.use({
+ suiteId: __filename
+});
+test.describe('Variables Pane', { tag: ['@web', '@win', '@pr'] }, () => {
+ test.beforeEach(async function ({ app }) {
+ await app.workbench.positronLayouts.enterLayout('stacked');
});
- describe('R Variables Pane #web #pr', () => {
-
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
+ test('Python - Verifies Variables pane basic function [C628634]', async function ({ app, logger, python }) {
+ const executeCode = async (code: string) => {
+ await app.workbench.positronConsole.executeCode('Python', code, '>>>');
+ };
- });
+ await executeCode('x=1');
+ await executeCode('y=10');
+ await executeCode('z=100');
- beforeEach(async function () {
- await this.app.workbench.positronLayouts.enterLayout('stacked');
+ logger.log('Entered lines in console defining variables');
+ await app.workbench.positronConsole.logConsoleContents();
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ const variablesMap = await app.workbench.positronVariables.getFlatVariables();
- });
+ expect(variablesMap.get('x')).toStrictEqual({ value: '1', type: 'int' });
+ expect(variablesMap.get('y')).toStrictEqual({ value: '10', type: 'int' });
+ expect(variablesMap.get('z')).toStrictEqual({ value: '100', type: 'int' });
- it('Verifies Variables pane basic function with R interpreter [C628635]', async function () {
- const app = this.app as Application;
-
- const executeCode = async (code: string) => {
- await app.workbench.positronConsole.executeCode('R', code, '>');
- };
-
- await executeCode('x=1');
- await executeCode('y=10');
- await executeCode('z=100');
-
- logger.log('Entered lines in console defining variables');
-
- await app.workbench.positronConsole.logConsoleContents();
+ });
- await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ test('R - Verifies Variables pane basic function [C628635]', async function ({ app, logger, r }) {
+ const executeCode = async (code: string) => {
+ await app.workbench.positronConsole.executeCode('R', code, '>');
+ };
- const variablesMap = await app.workbench.positronVariables.getFlatVariables();
+ await executeCode('x=1');
+ await executeCode('y=10');
+ await executeCode('z=100');
- expect(variablesMap.get('x')).toStrictEqual({ value: '1', type: 'dbl' });
- expect(variablesMap.get('y')).toStrictEqual({ value: '10', type: 'dbl' });
- expect(variablesMap.get('z')).toStrictEqual({ value: '100', type: 'dbl' });
- });
+ logger.log('Entered lines in console defining variables');
+ await app.workbench.positronConsole.logConsoleContents();
+ await app.workbench.positronLayouts.enterLayout('fullSizedAuxBar');
+ const variablesMap = await app.workbench.positronVariables.getFlatVariables();
+ expect(variablesMap.get('x')).toStrictEqual({ value: '1', type: 'dbl' });
+ expect(variablesMap.get('y')).toStrictEqual({ value: '10', type: 'dbl' });
+ expect(variablesMap.get('z')).toStrictEqual({ value: '100', type: 'dbl' });
});
});
diff --git a/test/smoke/src/areas/positron/viewer/viewer.test.ts b/test/smoke/src/areas/positron/viewer/viewer.test.ts
index ccbe451136c..2a9aed2c5ff 100644
--- a/test/smoke/src/areas/positron/viewer/viewer.test.ts
+++ b/test/smoke/src/areas/positron/viewer/viewer.test.ts
@@ -3,142 +3,106 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
+import { test } from '../_test.setup';
-describe('Viewer', () => {
- const logger = setupAndStartApp();
- let app: Application;
-
- describe('Viewer - Python', () => {
- beforeEach(async function () {
- app = this.app as Application;
+test.use({
+ suiteId: __filename
+});
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
+test.describe('Viewer', () => {
- it('Python - Verify Viewer functionality with vetiver [C784887]', async function () {
+ test.afterEach(async function ({ app }) {
+ await app.workbench.positronViewer.clearViewer();
+ });
- this.timeout(120000);
+ test('Python - Verify Viewer functionality with vetiver [C784887]', async function ({ app, logger, python }) {
+ logger.log('Sending code to console');
+ await app.workbench.positronConsole.pasteCodeToConsole(pythonScript);
+ await app.workbench.positronConsole.sendEnterKey();
+ const theDoc = app.workbench.positronViewer.getViewerLocator('#thedoc');
+ await theDoc.waitFor({ state: 'attached', timeout: 60000 });
- const script = `from vetiver import VetiverModel, VetiverAPI
-from vetiver.data import mtcars
-from sklearn.linear_model import LinearRegression
+ // This is bad because it can end up clicking a link inside the console:
+ //await app.workbench.positronConsole.activeConsole.click();
-model = LinearRegression().fit(mtcars.drop(columns="mpg"), mtcars["mpg"])
-v = VetiverModel(model, model_name = "cars_linear", prototype_data = mtcars.drop(columns="mpg"))
-VetiverAPI(v).run()`;
+ await app.workbench.positronConsole.clickConsoleTab();
+ await app.workbench.positronConsole.sendKeyboardKey('Control+C');
+ await app.workbench.positronConsole.waitForConsoleContents(buffer => buffer.some(line => line.includes('Application shutdown complete.')));
+ });
- logger.log('Sending code to console');
- await app.workbench.positronConsole.pasteCodeToConsole(script);
- await app.workbench.positronConsole.sendEnterKey();
+ // This randomly fails only in CI
+ test.skip('Python - Verify Viewer functionality with great-tables [C784888]', async function ({ app, logger, python }) {
- const theDoc = app.workbench.positronViewer.getViewerLocator('#thedoc');
+ // extra clean up - https://github.com/posit-dev/positron/issues/4604
+ // without this, on ubuntu, the Enter key send to the console
+ // won't work because the pasted code is out of view
+ await app.workbench.positronConsole.barClearButton.click();
- await theDoc.waitFor({ state: 'attached', timeout: 60000 });
+ logger.log('Sending code to console');
+ await app.workbench.positronConsole.pasteCodeToConsole(pythonGreatTablesScript);
+ await app.workbench.positronConsole.sendEnterKey();
- // This is bad because it can end up clicking a link inside the console:
- //await app.workbench.positronConsole.activeConsole.click();
+ const apricot = app.workbench.positronViewer.getViewerLocator('td').filter({ hasText: 'apricot' });
+ await apricot.waitFor({ state: 'attached', timeout: 60000 });
- await app.workbench.positronConsole.clickConsoleTab();
+ // Note that there is not a control to clear the viewer at this point
+ });
- await app.workbench.positronConsole.sendKeyboardKey('Control+C');
- await app.workbench.positronConsole.waitForConsoleContents(buffer => buffer.some(line => line.includes('Application shutdown complete.')));
+ test('R - Verify Viewer functionality with modelsummary [C784889]', async function ({ app, logger, r }) {
+ logger.log('Sending code to console');
+ await app.workbench.positronConsole.executeCode('R', rModelSummaryScript, '>');
+ const billDepthLocator = app.workbench.positronViewer.getViewerLocator('tr').filter({ hasText: 'bill_depth_mm' });
+ await billDepthLocator.waitFor({ state: 'attached' });
- await app.workbench.positronViewer.clearViewer();
+ });
- });
+ test('R - Verify Viewer functionality with reactable [C784930]', async function ({ app, logger, r }) {
- // This randomly fails only in CI
- it.skip('Python - Verify Viewer functionality with great-tables [C784888]', async function () {
- this.timeout(120000);
+ logger.log('Sending code to console');
+ await app.workbench.positronConsole.executeCode('R', rReactableScript, '>');
- // extra clean up - https://github.com/posit-dev/positron/issues/4604
- // without this, on ubuntu, the Enter key send to the console
- // won't work because the pasted code is out of view
- await app.workbench.positronConsole.barClearButton.click();
+ const datsun710 = app.workbench.positronViewer.getViewerLocator('div.rt-td-inner').filter({ hasText: 'Datsun 710' });
- const script = `from great_tables import GT, exibble
-GT(exibble)`;
+ await datsun710.waitFor({ state: 'attached' });
- logger.log('Sending code to console');
- await app.workbench.positronConsole.pasteCodeToConsole(script);
- await app.workbench.positronConsole.sendEnterKey();
+ });
- const apricot = app.workbench.positronViewer.getViewerLocator('td').filter({ hasText: 'apricot' });
+ test('R - Verify Viewer functionality with reprex [C784931]', async function ({ app, logger, r }) {
- await apricot.waitFor({ state: 'attached', timeout: 60000 });
+ logger.log('Sending code to console');
+ await app.workbench.positronConsole.executeCode('R', rReprexScript, '>');
- // Note that there is not a control to clear the viewer at this point
+ const rnorm = app.workbench.positronViewer.getViewerLocator('code.sourceCode').filter({ hasText: 'x <- rnorm(100)' });
- });
+ await rnorm.waitFor({ state: 'attached' });
});
-});
-
-describe('Viewer', () => {
- const logger = setupAndStartApp();
- let app: Application;
-
- describe('Viewer - R', () => {
- before(async function () {
- app = this.app as Application;
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- after(async function () {
- await app.workbench.positronViewer.clearViewer();
+});
- });
+const pythonScript = `from vetiver import VetiverModel, VetiverAPI
+from vetiver.data import mtcars
+from sklearn.linear_model import LinearRegression
+model = LinearRegression().fit(mtcars.drop(columns="mpg"), mtcars["mpg"])
+v = VetiverModel(model, model_name = "cars_linear", prototype_data = mtcars.drop(columns="mpg"))
+VetiverAPI(v).run()`;
- it('R - Verify Viewer functionality with modelsummary [C784889]', async function () {
+const pythonGreatTablesScript = `from great_tables import GT, exibble
+GT(exibble)`;
- const script = `library(palmerpenguins)
+const rModelSummaryScript = `library(palmerpenguins)
library(fixest)
library(modelsummary)
m1 = feols(body_mass_g ~ bill_depth_mm + bill_length_mm | species, data = penguins)
modelsummary(m1)`;
- logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('R', script, '>');
-
- const billDepthLocator = app.workbench.positronViewer.getViewerLocator('tr').filter({ hasText: 'bill_depth_mm' });
-
- await billDepthLocator.waitFor({ state: 'attached' });
-
- });
-
- it('R - Verify Viewer functionality with reactable [C784930]', async function () {
-
- const script = `library(reactable)
+const rReactableScript = `library(reactable)
mtcars |> reactable::reactable()`;
- logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('R', script, '>');
-
- const datsun710 = app.workbench.positronViewer.getViewerLocator('div.rt-td-inner').filter({ hasText: 'Datsun 710' });
- await datsun710.waitFor({ state: 'attached' });
-
- });
-
- it('R - Verify Viewer functionality with reprex [C784931]', async function () {
-
- const script = `reprex::reprex({
-x <- rnorm(100)
-plot(x, sin(x))
-})`;
-
- logger.log('Sending code to console');
- await app.workbench.positronConsole.executeCode('R', script, '>');
-
- const rnorm = app.workbench.positronViewer.getViewerLocator('code.sourceCode').filter({ hasText: 'x <- rnorm(100)' });
-
- await rnorm.waitFor({ state: 'attached' });
-
- });
- });
-});
+const rReprexScript = `reprex::reprex({
+ x <- rnorm(100)
+ plot(x, sin(x))
+ })`;
diff --git a/test/smoke/src/areas/positron/welcome/welcome.test.ts b/test/smoke/src/areas/positron/welcome/welcome.test.ts
index 91f67e84787..fc50ebcd3c1 100644
--- a/test/smoke/src/areas/positron/welcome/welcome.test.ts
+++ b/test/smoke/src/areas/positron/welcome/welcome.test.ts
@@ -3,31 +3,23 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
+import { test, expect } from '../_test.setup';
-import { expect } from '@playwright/test';
-import { Application, PositronPythonFixtures, PositronRFixtures } from '../../../../../automation';
-import { setupAndStartApp } from '../../../test-runner/test-hooks';
-
-describe('Welcome Page', () => {
- setupAndStartApp();
-
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
+test.use({
+ suiteId: __filename
+});
- beforeEach(async function () {
- const app = this.app as Application;
+test.describe('Welcome Page', () => {
+ test.beforeEach(async function ({ app }) {
await app.workbench.quickaccess.runCommand('Help: Welcome');
});
- afterEach(async function () {
- const app = this.app as Application;
+ test.afterEach(async function ({ app }) {
await app.workbench.quickaccess.runCommand('View: Close All Editors');
});
- describe('General', () => {
- it('Verify Welcome page header and footer [C684750]', async function () {
- const app = this.app as Application;
+ test.describe('General', () => {
+ test('Verify Welcome page header and footer [C684750]', async function ({ app }) {
await expect(app.workbench.positronWelcome.logo).toBeVisible();
@@ -37,8 +29,7 @@ describe('Welcome Page', () => {
await expect(app.workbench.positronWelcome.footer).toHaveText('Show welcome page on startup');
});
- it('Verify Welcome page content [C610960]', async function () {
- const app = this.app as Application;
+ test('Verify Welcome page content [C610960]', async function ({ app }) {
const OPEN_BUTTONS_LABELS = process.platform === 'darwin' ?
['Open...', 'New Folder...', 'New Folder from Git...']
: ['Open File...', 'Open Folder...', 'New Folder...', 'New Folder from Git...'];
@@ -63,9 +54,7 @@ describe('Welcome Page', () => {
await expect(app.workbench.positronWelcome.recentSection.locator('.empty-recent')).toHaveText('You have no recent folders,open a folderto start.');
});
- it('Click on new project from the Welcome page [C684751]', async function () {
- const app = this.app as Application;
-
+ test('Click on new project from the Welcome page [C684751]', async function ({ app }) {
await app.workbench.positronWelcome.newProjectButton.click();
await app.workbench.positronPopups.popupCurrentlyOpen();
@@ -78,13 +67,8 @@ describe('Welcome Page', () => {
});
});
- describe('Python', () => {
- before(async function () {
- await PositronPythonFixtures.SetupFixtures(this.app as Application);
- });
-
- it('Create a new Python file from the Welcome page [C684752]', async function () {
- const app = this.app as Application;
+ test.describe('Python', () => {
+ test('Create a new Python file from the Welcome page [C684752]', async function ({ app, python }) {
await app.workbench.positronWelcome.newFileButton.click();
@@ -95,8 +79,7 @@ describe('Welcome Page', () => {
await app.workbench.quickaccess.runCommand('View: Close Editor');
});
- it('Create a new Python notebook from the Welcome page [C684753]', async function () {
- const app = this.app as Application;
+ test('Create a new Python notebook from the Welcome page [C684753]', async function ({ app, python }) {
await app.workbench.positronWelcome.newNotebookButton.click();
@@ -108,8 +91,7 @@ describe('Welcome Page', () => {
await expect(app.workbench.positronNotebooks.kernelLabel).toHaveText(expectedInterpreterVersion);
});
- it('Click on Python console from the Welcome page [C684754]', async function () {
- const app = this.app as Application;
+ test('Click on Python console from the Welcome page [C684754]', async function ({ app, python }) {
await app.workbench.positronWelcome.newConsoleButton.click();
await app.workbench.positronPopups.popupCurrentlyOpen();
@@ -125,14 +107,8 @@ describe('Welcome Page', () => {
});
});
- describe('R', () => {
- before(async function () {
- await PositronRFixtures.SetupFixtures(this.app as Application);
- });
-
- it('Create a new R file from the Welcome page [C684755]', async function () {
- const app = this.app as Application;
-
+ test.describe('R', () => {
+ test('Create a new R file from the Welcome page [C684755]', async function ({ app, r }) {
await app.workbench.positronWelcome.newFileButton.click();
await app.workbench.quickinput.selectQuickInputElementContaining('R File');
@@ -140,9 +116,7 @@ describe('Welcome Page', () => {
await expect(app.workbench.editors.activeEditor.locator(app.workbench.editors.editorIcon)).toHaveClass(/r-lang-file-icon/);
});
- it('Click on R console from the Welcome page [C684756]', async function () {
- const app = this.app as Application;
-
+ test('Click on R console from the Welcome page [C684756]', async function ({ app, r }) {
await app.workbench.positronWelcome.newConsoleButton.click();
await app.workbench.positronPopups.popupCurrentlyOpen();
@@ -156,9 +130,7 @@ describe('Welcome Page', () => {
await expect(app.workbench.positronLayouts.panelViewsTab.and(app.code.driver.getLocator('.checked'))).toHaveText('Console');
});
- it('Create a new R notebook from the Welcome page [C684757]', async function () {
- const app = this.app as Application;
-
+ test('Create a new R notebook from the Welcome page [C684757]', async function ({ app, r }) {
await app.workbench.positronWelcome.newNotebookButton.click();
await app.workbench.positronPopups.clickOnModalDialogPopupOption('R Notebook');
diff --git a/test/smoke/src/test-runner/config.ts b/test/smoke/src/test-runner/config.ts
deleted file mode 100644
index 89b9dc90236..00000000000
--- a/test/smoke/src/test-runner/config.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import * as path from 'path';
-import * as os from 'os';
-import minimist = require('minimist');
-
-export const OPTS = minimist(process.argv.slice(2));
-const TEST_DATA_PATH = path.join(os.tmpdir(), 'vscsmoke');
-const ARTIFACT_DIR = process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || 'smoke-tests-default';
-const ROOT_PATH = path.join(__dirname, '..', '..', '..', '..');
-
-// Set environment variables
-Object.assign(process.env, {
- BUILD: OPTS['build'] || '',
- HEADLESS: OPTS['headless'] || '',
- PARALLEL: OPTS['parallel'] || '',
- REMOTE: OPTS['remote'] || '',
- TRACING: OPTS['tracing'] || '',
- VERBOSE: OPTS['verbose'] || '',
- WEB: OPTS['web'] || '',
- WIN: OPTS['win'] || '',
- ONLY: OPTS['only'] || '',
- PR: OPTS['pr'] || '',
- SKIP_CLEANUP: OPTS['skip-cleanup'] || '',
- TEST_DATA_PATH: TEST_DATA_PATH,
- EXTENSIONS_PATH: path.join(TEST_DATA_PATH, 'extensions-dir'),
- WORKSPACE_PATH: path.join(TEST_DATA_PATH, 'qa-example-content'),
- REPORT_PATH: path.join(ROOT_PATH, '.build', 'logs', ARTIFACT_DIR, 'test-results'),
- LOGS_ROOT_PATH: path.join(ROOT_PATH, '.build', 'logs', ARTIFACT_DIR),
- CRASHES_ROOT_PATH: path.join(ROOT_PATH, '.build', 'crashes', ARTIFACT_DIR),
-});
-
diff --git a/test/smoke/src/test-runner/index.ts b/test/smoke/src/test-runner/index.ts
index 0393c01d135..b62cc22aa9a 100644
--- a/test/smoke/src/test-runner/index.ts
+++ b/test/smoke/src/test-runner/index.ts
@@ -5,4 +5,3 @@
export { prepareTestEnv } from './test-setup';
export { cloneTestRepo } from './utils';
-export { runMochaTests } from './mocha-runner';
diff --git a/test/smoke/src/test-runner/logger.ts b/test/smoke/src/test-runner/logger.ts
index 8e0a521629c..31b44948838 100644
--- a/test/smoke/src/test-runner/logger.ts
+++ b/test/smoke/src/test-runner/logger.ts
@@ -17,7 +17,7 @@ const VERBOSE = process.env.VERBOSE === 'true';
* @returns Logger instance
*/
export function createLogger(logsRootPath: string): Logger {
- const logsFileName = `smoke-test-runner.log`;
+ const logsFileName = `e2e-test-runner.log`;
const loggers: Logger[] = [];
if (VERBOSE) {
@@ -64,7 +64,7 @@ function logToFile(logFilePath: string, message: string): void {
* @param err error
*/
export function logErrorToFile(test: any, err: Error): void {
- const LOGS_ROOT_PATH = process.env.LOGS_ROOT_PATH || 'LOGS_ROOT_PATH not set';
+ const LOGS_ROOT_PATH = process.env.LOGS_ROOT_PATH || 'LOGS_ROOT_PATH not set logger';
const fileName = path.basename(test.file);
const testLogPath = path.join(LOGS_ROOT_PATH, fileName, 'retry.log');
diff --git a/test/smoke/src/test-runner/mocha-runner.ts b/test/smoke/src/test-runner/mocha-runner.ts
deleted file mode 100644
index b23eea8eb8d..00000000000
--- a/test/smoke/src/test-runner/mocha-runner.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import * as path from 'path';
-// eslint-disable-next-line local/code-import-patterns
-import * as fs from 'fs/promises';
-import { logErrorToFile } from './logger';
-const Mocha = require('mocha');
-
-const TEST_DATA_PATH = process.env.TEST_DATA_PATH || 'TEST_DATA_PATH not set';
-const REPORT_PATH = process.env.REPORT_PATH || 'REPORT_PATH not set';
-
-/**
- * Runs Mocha tests.
- */
-export async function runMochaTests(OPTS: any) {
- const mocha = new Mocha({
- color: true,
- timeout: 1 * 60 * 1000, // 1 minute
- slow: 30 * 1000, // 30 seconds
- grep: OPTS['f'] || OPTS['g'],
- parallel: OPTS['parallel'],
- jobs: OPTS['jobs'],
- reporter: 'mocha-multi',
- reporterOptions: {
- spec: '-', // Console output
- xunit: path.join(REPORT_PATH, 'xunit-results.xml')
- },
- retries: 0,
- });
-
- // Apply test filters based on CLI options
- applyTestFilters(mocha);
-
- // Add test files to the Mocha runner
- const testFiles = await findTestFilesRecursive(path.resolve('out/areas/positron'));
- testFiles.forEach(file => mocha.addFile(file));
-
- // Run the Mocha tests
- const runner = mocha.run(async failures => {
- if (failures) {
- console.log(getFailureLogs());
- } else {
- console.log('All tests passed.');
- }
-
- await cleanupTestData();
- process.exit(failures ? 1 : 0);
- });
-
- runner.on('retry', (test, err) => {
- logErrorToFile(test, err);
- });
-
- runner.on('fail', (test, err) => {
- logErrorToFile(test, err);
- });
-}
-
-/**
- * Applies test filters based on environment variables.
- */
-function applyTestFilters(mocha: Mocha): void {
- // TODO: see if it's possible to use multiple filters
- const filters = {
- WEB: /#web/,
- WIN: /#win/,
- PR: /#pr/,
- ONLY: /#only/
- };
-
- Object.keys(filters).forEach((key) => {
- if (process.env[key]) {
- mocha.grep(filters[key]);
- }
- });
-
- if (process.env.INVERSE_FILTER) {
- mocha.grep(process.env.INVERSE_FILTER).invert();
- }
-}
-
-/**
- * Recursively finds all test files in child directories.
- */
-async function findTestFilesRecursive(dirPath: string): Promise {
- let testFiles: string[] = [];
-
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
-
- for (const entry of entries) {
- const fullPath = path.join(dirPath, entry.name);
- if (entry.isDirectory()) {
- // If it's a directory, recursively search within it
- const subDirFiles = await findTestFilesRecursive(fullPath);
- testFiles = testFiles.concat(subDirFiles);
- } else if (entry.isFile() && entry.name.endsWith('.js') && !entry.name.startsWith('example.test.js')) {
- // If it's a file, add it if it matches the criteria
- testFiles.push(fullPath);
- }
- }
-
- return testFiles;
-}
-
-/**
- * Cleans up the test data directory.
- */
-async function cleanupTestData(): Promise {
- if (process.env.SKIP_CLEANUP) {
- console.log('Skipping test data cleanup.');
- return;
- }
-
- try {
- console.log('Cleaning up test data directory. FYI: This can be bypassed with --skip-cleanup');
- await fs.rm(TEST_DATA_PATH, { recursive: true, force: true });
- console.log('Cleanup completed successfully.');
- } catch (error) {
- console.error(`Error cleaning up test data: ${error}`);
- }
-}
-
-/**
- * Returns formatted failure log messages.
- */
-function getFailureLogs(): string {
- const rootPath = path.join(__dirname, '..', '..', '..');
- const logPath = path.join(rootPath, '.build', 'logs');
-
- if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
- return `
-###################################################################
-# #
-# Logs are attached as build artefact and can be downloaded #
-# from the build Summary page (Summary -> Related -> N published) #
-# #
-# Show playwright traces on: https://trace.playwright.dev/ #
-# #
-###################################################################
- `;
- } else {
- return `
-#############################################
-#
-# Log files of client & server are stored into
-# '${logPath}'.
-#
-# Logs of the smoke test runner are stored into
-# 'smoke-test-runner.log' in respective folder.
-#
-#############################################
- `;
- }
-}
diff --git a/test/smoke/src/test-runner/test-hooks.ts b/test/smoke/src/test-runner/test-hooks.ts
deleted file mode 100644
index d0899c7b631..00000000000
--- a/test/smoke/src/test-runner/test-hooks.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (C) 2024 Posit Software, PBC. All rights reserved.
- * Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
- *--------------------------------------------------------------------------------------------*/
-
-import { join } from 'path';
-import * as path from 'path';
-import { Logger, } from '../../../automation';
-import { installAllHandlers, } from '../utils';
-import { createLogger } from './logger';
-
-export const ROOT_PATH = join(__dirname, '..', '..', '..', '..');
-const TEST_DATA_PATH = process.env.TEST_DATA_PATH || 'TEST_DATA_PATH not set';
-const WORKSPACE_PATH = process.env.WORKSPACE_PATH || 'WORKSPACE_PATH not set';
-const EXTENSIONS_PATH = process.env.EXTENSIONS_PATH || 'EXTENSIONS_PATH not set';
-const LOGS_ROOT_PATH = process.env.LOGS_ROOT_PATH || 'LOGS_ROOT_PATH not set';
-const CRASHES_ROOT_PATH = process.env.CRASHES_ROOT_PATH || 'CRASHES_ROOT_PATH not set';
-
-const asBoolean = (value: string | undefined): boolean | undefined => {
- return value === 'true' ? true : value === 'false' ? false : undefined;
-};
-
-// NOTE: When running with --build, the config file does not load so we are setting the options here
-const OPTS: ParseOptions = {
- tracing: asBoolean(process.env.TRACING),
- parallel: asBoolean(process.env.PARALLEL),
- web: asBoolean(process.env.WEB),
- build: process.env.BUILD,
- remote: asBoolean(process.env.REMOTE),
- verbose: asBoolean(process.env.VERBOSE),
- headless: asBoolean(process.env.HEADLESS),
- browser: process.env.BROWSER,
- electronArgs: process.env.ELECTRON_ARGS,
- version: process.env.BUILD_VERSION,
-};
-
-/**
- * Setup the environment, logs, hooks for the test suite and then START the application.
- *
- * @returns The logger instance for the test suite.
- */
-export function setupAndStartApp(): Logger {
- // Dynamically determine the test file name
- const suiteName = getTestFileName();
- const logsRootPath = join(LOGS_ROOT_PATH, suiteName);
- const crashesRootPath = join(CRASHES_ROOT_PATH, suiteName);
-
- // Create a new logger for this suite
- const logger = createLogger(logsRootPath);
-
- // Set test defaults and before/after hooks
- setTestDefaults(logger, logsRootPath, crashesRootPath);
- installAllHandlers(logger);
-
- return logger;
-}
-
-/**
- * Set the default options for the test suite.
- *
- * @param logger the logger instance for the test suite
- * @param logsRootPath the root path for the logs
- * @param crashesRootPath the root path for the crashes
- */
-function setTestDefaults(logger: Logger, logsRootPath: string, crashesRootPath: string) {
- before(async function () {
- this.defaultOptions = {
- codePath: OPTS.build,
- workspacePath: WORKSPACE_PATH,
- userDataDir: join(TEST_DATA_PATH, 'd'),
- extensionsPath: EXTENSIONS_PATH,
- logger,
- logsPath: join(logsRootPath, 'suite_unknown'),
- crashesPath: join(crashesRootPath, 'suite_unknown'),
- verbose: OPTS.verbose,
- remote: OPTS.remote,
- web: OPTS.web,
- tracing: OPTS.tracing,
- headless: OPTS.headless,
- browser: OPTS.browser,
- extraArgs: (OPTS.electronArgs || '').split(' ').map(arg => arg.trim()).filter(arg => !!arg),
- };
- });
-}
-
-/**
- * Dynamically determines the test file path based on the caller's stack trace.
- *
- * @returns The file name of the test file.
- */
-function getTestFileName(): string {
- const originalFunc = Error.prepareStackTrace;
-
- try {
- // Capture the stack trace
- const err = new Error();
- Error.prepareStackTrace = (_, stack) => stack;
-
- // Stack index 2 points to the immediate caller of this function
- const stackFrames = err.stack as any;
- const callerFilePath = stackFrames[2].getFileName(); // Adjust index based on context
-
- return path.basename(callerFilePath);
- } catch (e) {
- console.error('Failed to retrieve caller file name:', e);
- return 'unknown';
- } finally {
- // Restore the original stack trace behavior
- Error.prepareStackTrace = originalFunc;
- }
-}
-
-type ParseOptions = {
- verbose?: boolean;
- remote?: boolean;
- headless?: boolean;
- web?: boolean;
- tracing?: boolean;
- parallel?: boolean;
- build?: string;
- 'stable-build'?: string;
- browser?: string;
- electronArgs?: string;
- version?: string;
-};
diff --git a/test/smoke/src/test-runner/test-setup.ts b/test/smoke/src/test-runner/test-setup.ts
index c5a3e8c58a0..a9075b9c905 100644
--- a/test/smoke/src/test-runner/test-setup.ts
+++ b/test/smoke/src/test-runner/test-setup.ts
@@ -9,9 +9,9 @@ const rimraf = require('rimraf');
const mkdirp = require('mkdirp');
import { getBuildElectronPath, getDevElectronPath, Logger } from '../../../automation';
import { createLogger } from './logger';
+import * as os from 'os';
-const ROOT_PATH = process.env.ROOT_PATH || 'ROOT_PATH not set';
-const TEST_DATA_PATH = process.env.TEST_DATA_PATH || 'TEST_DATA_PATH not set';
+const TEST_DATA_PATH = join(os.tmpdir(), 'vscsmoke');
const WEB = process.env.WEB;
const REMOTE = process.env.REMOTE;
const BUILD = process.env.BUILD;
@@ -22,12 +22,12 @@ const BUILD = process.env.BUILD;
* 2. initializes the test environment
* 3. prepares the test data directory
*/
-export function prepareTestEnv() {
- const logsRootPath = join(ROOT_PATH, '.build', 'logs', 'test-setup');
+export function prepareTestEnv(rootPath = process.env.ROOT_PATH || 'ROOT_PATH not set prepareTestEnv') {
+ const logsRootPath = join(rootPath, '.build', 'logs', 'test-setup');
const logger = createLogger(logsRootPath);
try {
- initializeTestEnvironment(logger);
+ initializeTestEnvironment(rootPath, logger);
console.log('Test environment setup completed successfully.');
// Disabling this section of code for now. It's used to download a stable version of VSCode
@@ -48,7 +48,7 @@ export function prepareTestEnv() {
/**
* Sets up the test environment for Electron or Web smoke tests.
*/
-function initializeTestEnvironment(logger: Logger): string | null {
+function initializeTestEnvironment(rootPath = process.env.ROOT_PATH || 'ROOT_PATH not set initTestEnv', logger: Logger): string | null {
let version: string | null = null;
//
@@ -66,7 +66,7 @@ function initializeTestEnvironment(logger: Logger): string | null {
} else {
testCodePath = getDevElectronPath();
electronPath = testCodePath;
- process.env.VSCODE_REPOSITORY = ROOT_PATH;
+ process.env.VSCODE_REPOSITORY = rootPath;
process.env.VSCODE_DEV = '1';
process.env.VSCODE_CLI = '1';
}
@@ -97,7 +97,7 @@ function initializeTestEnvironment(logger: Logger): string | null {
}
if (!testCodeServerPath) {
- process.env.VSCODE_REPOSITORY = ROOT_PATH;
+ process.env.VSCODE_REPOSITORY = rootPath;
process.env.VSCODE_DEV = '1';
process.env.VSCODE_CLI = '1';
diff --git a/test/smoke/src/test-runner/utils.ts b/test/smoke/src/test-runner/utils.ts
index 3ed8dc064a9..651b34127ca 100644
--- a/test/smoke/src/test-runner/utils.ts
+++ b/test/smoke/src/test-runner/utils.ts
@@ -8,41 +8,40 @@ import * as fs from 'fs';
const rimraf = require('rimraf');
const TEST_REPO = process.env.TEST_REPO;
-const WORKSPACE_PATH = process.env.WORKSPACE_PATH || 'WORKSPACE_PATH is not set';
/**
* Clones or copies the test repository based on options.
*/
-export function cloneTestRepo() {
+export function cloneTestRepo(workspacePath = process.env.WORKSPACE_PATH || 'WORKSPACE_PATH is not set cloneRepo') {
const testRepoUrl = 'https://github.com/posit-dev/qa-example-content.git';
if (TEST_REPO) {
console.log('Copying test project repository from:', TEST_REPO);
// Remove the existing workspace path if the option is provided
- rimraf.sync(WORKSPACE_PATH);
+ rimraf.sync(workspacePath);
// Copy the repository based on the platform (Windows vs. non-Windows)
if (process.platform === 'win32') {
- cp.execSync(`xcopy /E "${TEST_REPO}" "${WORKSPACE_PATH}\\*"`);
+ cp.execSync(`xcopy /E "${TEST_REPO}" "${workspacePath}\\*"`);
} else {
- cp.execSync(`cp -R "${TEST_REPO}" "${WORKSPACE_PATH}"`);
+ cp.execSync(`cp -R "${TEST_REPO}" "${workspacePath}"`);
}
} else {
// If no test-repo is specified, clone the repository if it doesn't exist
- if (!fs.existsSync(WORKSPACE_PATH)) {
+ if (!fs.existsSync(workspacePath)) {
console.log('Cloning test project repository from:', testRepoUrl);
- const res = cp.spawnSync('git', ['clone', testRepoUrl, WORKSPACE_PATH], { stdio: 'inherit' });
+ const res = cp.spawnSync('git', ['clone', testRepoUrl, workspacePath], { stdio: 'inherit' });
// Check if cloning failed by verifying if the workspacePath was created
- if (!fs.existsSync(WORKSPACE_PATH)) {
+ if (!fs.existsSync(workspacePath)) {
throw new Error(`Clone operation failed: ${res.stderr?.toString()}`);
}
} else {
console.log('Cleaning and updating test project repository...');
// Fetch the latest changes, reset to the latest commit, and clean the repo
- cp.spawnSync('git', ['fetch'], { cwd: WORKSPACE_PATH, stdio: 'inherit' });
- cp.spawnSync('git', ['reset', '--hard', 'FETCH_HEAD'], { cwd: WORKSPACE_PATH, stdio: 'inherit' });
- cp.spawnSync('git', ['clean', '-xdf'], { cwd: WORKSPACE_PATH, stdio: 'inherit' });
+ cp.spawnSync('git', ['fetch'], { cwd: workspacePath, stdio: 'inherit' });
+ cp.spawnSync('git', ['reset', '--hard', 'FETCH_HEAD'], { cwd: workspacePath, stdio: 'inherit' });
+ cp.spawnSync('git', ['clean', '-xdf'], { cwd: workspacePath, stdio: 'inherit' });
}
}
}
diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock
index fb5810ee80e..0af6794ac3a 100644
--- a/test/smoke/yarn.lock
+++ b/test/smoke/yarn.lock
@@ -2,6 +2,18 @@
# yarn lockfile v1
+"@isaacs/cliui@^8.0.2":
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
+ integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
+ dependencies:
+ string-width "^5.1.2"
+ string-width-cjs "npm:string-width@^4.2.0"
+ strip-ansi "^7.0.1"
+ strip-ansi-cjs "npm:strip-ansi@^6.0.1"
+ wrap-ansi "^8.1.0"
+ wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
+
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa"
@@ -17,6 +29,11 @@
semver "^7.3.5"
tar "^6.1.11"
+"@pkgjs/parseargs@^0.11.0":
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
+ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
+
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@@ -93,6 +110,13 @@ abbrev@1:
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+abort-controller@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
+ integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+ dependencies:
+ event-target-shim "^5.0.0"
+
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -105,6 +129,11 @@ ansi-regex@^5.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+ansi-regex@^6.0.1:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
+ integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
+
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -112,11 +141,49 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
+ansi-styles@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+ansi-styles@^6.1.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
+ integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
+
"aproba@^1.0.3 || ^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+archiver-utils@^5.0.0, archiver-utils@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d"
+ integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==
+ dependencies:
+ glob "^10.0.0"
+ graceful-fs "^4.2.0"
+ is-stream "^2.0.1"
+ lazystream "^1.0.0"
+ lodash "^4.17.15"
+ normalize-path "^3.0.0"
+ readable-stream "^4.0.0"
+
+archiver@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61"
+ integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==
+ dependencies:
+ archiver-utils "^5.0.2"
+ async "^3.2.4"
+ buffer-crc32 "^1.0.0"
+ readable-stream "^4.0.0"
+ readdir-glob "^1.1.2"
+ tar-stream "^3.0.0"
+ zip-stream "^6.0.1"
+
are-we-there-yet@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
@@ -125,16 +192,36 @@ are-we-there-yet@^2.0.0:
delegates "^1.0.0"
readable-stream "^3.6.0"
+async@^3.2.4:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
+ integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
+
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+b4a@^1.6.4:
+ version "1.6.7"
+ resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4"
+ integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==
+
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+bare-events@^2.2.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc"
+ integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==
+
+base64-js@^1.3.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -143,6 +230,26 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
+brace-expansion@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+ integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+ dependencies:
+ balanced-match "^1.0.0"
+
+buffer-crc32@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405"
+ integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==
+
+buffer@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
+ integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.2.1"
+
call-bind@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
@@ -181,11 +288,23 @@ color-convert@^1.9.0:
dependencies:
color-name "1.1.3"
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
color-support@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
@@ -198,6 +317,17 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
+compress-commons@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e"
+ integrity sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==
+ dependencies:
+ crc-32 "^1.2.0"
+ crc32-stream "^6.0.0"
+ is-stream "^2.0.1"
+ normalize-path "^3.0.0"
+ readable-stream "^4.0.0"
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -208,6 +338,24 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0:
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
+core-util-is@~1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+crc-32@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
+ integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
+
+crc32-stream@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430"
+ integrity sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==
+ dependencies:
+ crc-32 "^1.2.0"
+ readable-stream "^4.0.0"
+
cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -219,6 +367,15 @@ cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
+cross-spawn@^7.0.0:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+ integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
debug@4:
version "4.3.5"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e"
@@ -255,11 +412,21 @@ detect-libc@^2.0.0:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
+eastasianwidth@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
+ integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+emoji-regex@^9.2.2:
+ version "9.2.2"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
+ integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@@ -299,6 +466,16 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+event-target-shim@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
+ integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
+events@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
exec-sh@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36"
@@ -306,6 +483,19 @@ exec-sh@^0.2.0:
dependencies:
merge "^1.2.0"
+fast-fifo@^1.2.0, fast-fifo@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
+ integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
+
+foreground-child@^3.1.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77"
+ integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==
+ dependencies:
+ cross-spawn "^7.0.0"
+ signal-exit "^4.0.1"
+
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
@@ -356,6 +546,18 @@ get-intrinsic@^1.0.0:
has "^1.0.3"
has-symbols "^1.0.1"
+glob@^10.0.0:
+ version "10.4.5"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
+ integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
+ dependencies:
+ foreground-child "^3.1.0"
+ jackspeak "^3.1.2"
+ minimatch "^9.0.4"
+ minipass "^7.1.2"
+ package-json-from-dist "^1.0.0"
+ path-scurry "^1.11.1"
+
glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
@@ -373,6 +575,11 @@ graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+graceful-fs@^4.2.0:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -408,6 +615,11 @@ https-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
+ieee754@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -416,7 +628,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@^2.0.3:
+inherits@2, inherits@^2.0.3, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -465,6 +677,11 @@ is-regex@^1.1.1:
dependencies:
has-symbols "^1.0.1"
+is-stream@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+ integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
@@ -472,16 +689,37 @@ is-symbol@^1.0.2:
dependencies:
has-symbols "^1.0.1"
+isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+jackspeak@^3.1.2:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
+ integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
+ dependencies:
+ "@isaacs/cliui" "^8.0.2"
+ optionalDependencies:
+ "@pkgjs/parseargs" "^0.11.0"
+
json-parse-better-errors@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+lazystream@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638"
+ integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==
+ dependencies:
+ readable-stream "^2.0.5"
+
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@@ -492,6 +730,16 @@ load-json-file@^4.0.0:
pify "^3.0.0"
strip-bom "^3.0.0"
+lodash@^4.17.15:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+lru-cache@^10.2.0:
+ version "10.4.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
+ integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
+
make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@@ -533,6 +781,20 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
+minimatch@^5.1.0:
+ version "5.1.6"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
+ integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
+ dependencies:
+ brace-expansion "^2.0.1"
+
+minimatch@^9.0.4:
+ version "9.0.5"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+ integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
+ dependencies:
+ brace-expansion "^2.0.1"
+
minimist@^1.2.0:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
@@ -550,6 +812,11 @@ minipass@^5.0.0:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
+"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
+ integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
+
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
@@ -607,6 +874,11 @@ normalize-package-data@^2.3.2:
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
+normalize-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
npm-run-all@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
@@ -664,6 +936,11 @@ once@^1.3.0, once@^1.3.1:
dependencies:
wrappy "1"
+package-json-from-dist@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
+ integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
+
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -682,11 +959,24 @@ path-key@^2.0.1:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
path-parse@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+path-scurry@^1.11.1:
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
+ integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
+ dependencies:
+ lru-cache "^10.2.0"
+ minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@@ -704,6 +994,21 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+queue-tick@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142"
+ integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==
+
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
@@ -713,6 +1018,19 @@ read-pkg@^3.0.0:
normalize-package-data "^2.3.2"
path-type "^3.0.0"
+readable-stream@^2.0.5:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
+ integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
readable-stream@^3.6.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
@@ -722,6 +1040,24 @@ readable-stream@^3.6.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
+readable-stream@^4.0.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09"
+ integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==
+ dependencies:
+ abort-controller "^3.0.0"
+ buffer "^6.0.3"
+ events "^3.3.0"
+ process "^0.11.10"
+ string_decoder "^1.3.0"
+
+readdir-glob@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584"
+ integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==
+ dependencies:
+ minimatch "^5.1.0"
+
resemblejs@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/resemblejs/-/resemblejs-5.0.0.tgz#f5a0c6aaa59dcfb9f5192e7ab8740616cbbbf220"
@@ -744,6 +1080,11 @@ rimraf@3.0.2, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@@ -776,11 +1117,23 @@ shebang-command@^1.2.0:
dependencies:
shebang-regex "^1.0.0"
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
shell-quote@^1.6.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
@@ -791,6 +1144,11 @@ signal-exit@^3.0.0:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+signal-exit@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
@@ -831,7 +1189,18 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65"
integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
+streamx@^2.15.0:
+ version "2.20.1"
+ resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.1.tgz#471c4f8b860f7b696feb83d5b125caab2fdbb93c"
+ integrity sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==
+ dependencies:
+ fast-fifo "^1.3.2"
+ queue-tick "^1.0.1"
+ text-decoder "^1.1.0"
+ optionalDependencies:
+ bare-events "^2.2.0"
+
+"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -840,6 +1209,24 @@ spdx-license-ids@^3.0.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^5.0.1, string-width@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
+ integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+ dependencies:
+ eastasianwidth "^0.2.0"
+ emoji-regex "^9.2.2"
+ strip-ansi "^7.0.1"
+
string.prototype.padend@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.1.tgz#824c84265dbac46cade2b957b38b6a5d8d1683c5"
@@ -865,20 +1252,41 @@ string.prototype.trimstart@^1.0.1:
call-bind "^1.0.0"
define-properties "^1.1.3"
-string_decoder@^1.1.1:
+string_decoder@^1.1.1, string_decoder@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
-strip-ansi@^6.0.1:
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
+strip-ansi@^7.0.1:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
+ integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
+ dependencies:
+ ansi-regex "^6.0.1"
+
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -891,6 +1299,15 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
+tar-stream@^3.0.0:
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b"
+ integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==
+ dependencies:
+ b4a "^1.6.4"
+ fast-fifo "^1.2.0"
+ streamx "^2.15.0"
+
tar@^6.1.11:
version "6.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
@@ -903,6 +1320,11 @@ tar@^6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"
+text-decoder@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.1.tgz#e173f5121d97bfa3ff8723429ad5ba92e1ead67e"
+ integrity sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==
+
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@@ -913,7 +1335,7 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
-util-deprecate@^1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
@@ -954,6 +1376,13 @@ which@^1.2.9:
dependencies:
isexe "^2.0.0"
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
wide-align@^1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
@@ -961,6 +1390,24 @@ wide-align@^1.1.2:
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrap-ansi@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
+ integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
+ dependencies:
+ ansi-styles "^6.1.0"
+ string-width "^5.0.1"
+ strip-ansi "^7.0.1"
+
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -970,3 +1417,12 @@ yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+zip-stream@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb"
+ integrity sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==
+ dependencies:
+ archiver-utils "^5.0.0"
+ compress-commons "^6.0.2"
+ readable-stream "^4.0.0"
diff --git a/yarn.lock b/yarn.lock
index ffb732c7ed9..6c1825933a4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,34 @@
# yarn lockfile v1
+"@actions/core@^1.10.0":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.11.1.tgz#ae683aac5112438021588030efb53b1adb86f172"
+ integrity sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==
+ dependencies:
+ "@actions/exec" "^1.1.1"
+ "@actions/http-client" "^2.0.1"
+
+"@actions/exec@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@actions/exec/-/exec-1.1.1.tgz#2e43f28c54022537172819a7cf886c844221a611"
+ integrity sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==
+ dependencies:
+ "@actions/io" "^1.0.1"
+
+"@actions/http-client@^2.0.1":
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.3.tgz#31fc0b25c0e665754ed39a9f19a8611fc6dab674"
+ integrity sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==
+ dependencies:
+ tunnel "^0.0.6"
+ undici "^5.25.4"
+
+"@actions/io@^1.0.1":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@actions/io/-/io-1.1.3.tgz#4cdb6254da7962b07473ff5c335f3da485d94d71"
+ integrity sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==
+
"@ampproject/remapping@^2.1.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
@@ -528,6 +556,11 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe"
integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==
+"@fastify/busboy@^2.0.0":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
+ integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
+
"@gulp-sourcemaps/identity-map@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz#a6e8b1abec8f790ec6be2b8c500e6e68037c0019"
@@ -746,6 +779,15 @@
resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz#7437db7aa061162ee94e4131b69a62b8dad5dea6"
integrity sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==
+"@midleman/github-actions-reporter@^1.9.5":
+ version "1.9.5"
+ resolved "https://registry.yarnpkg.com/@midleman/github-actions-reporter/-/github-actions-reporter-1.9.5.tgz#19820b3c7077fefb9cdcff217df699f8e906cd0f"
+ integrity sha512-X8ye2ipbfizZ9tZWEEpjarcCLqQAWH3E+klv8EYCtIbESm8hXuXYdAAo+2x1bSfS4bfjrwBTNh31yreYwKfsMg==
+ dependencies:
+ "@actions/core" "^1.10.0"
+ ansi-to-html "^0.7.2"
+ marked "^12.0.1"
+
"@nodelib/fs.scandir@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
@@ -916,12 +958,12 @@
dependencies:
playwright-core "1.46.0"
-"@playwright/test@^1.46.1":
- version "1.46.1"
- resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.46.1.tgz#a8dfdcd623c4c23bb1b7ea588058aad41055c188"
- integrity sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==
+"@playwright/test@^1.49.0":
+ version "1.49.0"
+ resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.0.tgz#74227385b58317ee076b86b56d0e1e1b25cff01e"
+ integrity sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==
dependencies:
- playwright "1.46.1"
+ playwright "1.49.0"
"@sindresorhus/is@^4.0.0":
version "4.6.0"
@@ -2300,6 +2342,13 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
+ansi-to-html@^0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.7.2.tgz#a92c149e4184b571eb29a0135ca001a8e2d710cb"
+ integrity sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==
+ dependencies:
+ entities "^2.2.0"
+
ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
@@ -4257,6 +4306,11 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
+entities@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+ integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
entities@^4.2.0, entities@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
@@ -7583,6 +7637,11 @@ markdown-it@^14.0.0:
punycode.js "^2.3.1"
uc.micro "^2.1.0"
+marked@^12.0.1:
+ version "12.0.2"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-12.0.2.tgz#b31578fe608b599944c69807b00f18edab84647e"
+ integrity sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==
+
matchdep@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
@@ -8885,17 +8944,17 @@ playwright-core@1.46.0:
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.46.0.tgz#2336ac453a943abf0dc95a76c117f9d3ebd390eb"
integrity sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==
-playwright-core@1.46.1:
- version "1.46.1"
- resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.46.1.tgz#28f3ab35312135dda75b0c92a3e5c0e7edb9cc8b"
- integrity sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==
+playwright-core@1.49.0:
+ version "1.49.0"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.0.tgz#8e69ffed3f41855b854982f3632f2922c890afcb"
+ integrity sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==
-playwright@1.46.1:
- version "1.46.1"
- resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.46.1.tgz#ea562bc48373648e10420a10c16842f0b227c218"
- integrity sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==
+playwright@1.49.0:
+ version "1.49.0"
+ resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.0.tgz#df6b9e05423377a99658202844a294a8afb95d0a"
+ integrity sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==
dependencies:
- playwright-core "1.46.1"
+ playwright-core "1.49.0"
optionalDependencies:
fsevents "2.3.2"
@@ -11372,6 +11431,13 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+undici@^5.25.4:
+ version "5.28.4"
+ resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
+ integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==
+ dependencies:
+ "@fastify/busboy" "^2.0.0"
+
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"