diff --git a/.github/workflows/plotlyTestCoverage_v9ChartsDownload.yml b/.github/workflows/plotlyTestCoverage_v9ChartsDownload.yml new file mode 100644 index 00000000000..8ef13554c4a --- /dev/null +++ b/.github/workflows/plotlyTestCoverage_v9ChartsDownload.yml @@ -0,0 +1,182 @@ +name: "[v9] Plotly Test coverage v9 Charts Download" +on: + schedule: + - cron: "30 6 * * *" # Runs every day at 12:00 PM IST (02:30 AM UTC) + workflow_dispatch: + inputs: + repo: + description: "Repo to run the tests on" + required: true + default: "microsoft/fluentui" + branch: + description: "Branch to run the tests on" + required: true + default: "master" + +permissions: + contents: write + pages: write + id-token: write + +jobs: + run_tests: + strategy: + matrix: + os: [windows-latest] + runs-on: ${{ matrix.os }} + outputs: + test_coverage: ${{ steps.run_tests.outputs }} + windows_artifact_name: ${{ steps.windows.outputs.COVERAGE_FILENAME_WINDOWS }} + + steps: + - name: Enable Git long paths + run: git config --global core.longpaths true + + - name: Checkout [react-charting] + uses: actions/checkout@v4 + with: + repository: ${{ github.event.inputs.repo || 'microsoft/fluentui'}} + ref: ${{ github.event.inputs.branch || 'master'}} + path: repo1 + + - name: Display Input Repo and Branch in Summary + shell: pwsh + run: | + echo "### Workflow Inputs" >> $env:GITHUB_STEP_SUMMARY + echo "- **Repository**: ${{ github.event.inputs.repo }}" >> $env:GITHUB_STEP_SUMMARY + echo "- **Branch**: ${{ github.event.inputs.branch }}" >> $env:GITHUB_STEP_SUMMARY + + - name: Show current directory + run: echo "$PWD" && ls + + - name: Show repo1 repository + run: ls ./repo1 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install packages + run: | + cd ./repo1 + yarn + + - name: Build + run: | + cd ./repo1 + yarn nx run react-charts:build + + - name: Run yarn pack in chart-utilities to create .tgz file + run: | + cd ./repo1/packages/charts/chart-utilities + yarn pack --filename chart-utilities.tgz + id: pack-util + + - name: Run yarn pack in react-charts to create .tgz file + run: | + cd ./repo1/packages/charts/react-charts/library + yarn pack --filename react-charts.tgz + id: pack-chart-v9-charts-download + + - name: Upload chart-utilities.tgz as artifact + uses: actions/upload-artifact@v4 + with: + name: chart-utilities-tgz + path: ./repo1/packages/charts/chart-utilities/chart-utilities.tgz + + - name: Upload react-charts.tgz as artifact + uses: actions/upload-artifact@v4 + with: + name: react-charts-tgz + path: ./repo1/packages/charts/react-charts/library/react-charts.tgz + + - name: Checkout [main] of current repo + uses: actions/checkout@v4 + with: + path: contrib_repo + + - name: Add chart-utils package to resolutions block + run: | + cd contrib_repo/apps/plotly_examples + npx json -I -f package.json -e "this.resolutions = this.resolutions || {}; this.resolutions['@fluentui/chart-utilities'] = 'file:../../../repo1/packages/charts/chart-utilities/chart-utilities.tgz';" + + + - name: Install .tgz file in Plotly examples + run: | + cd contrib_repo/apps/plotly_examples + yarn add ../../../repo1/packages/charts/react-charts/library/react-charts.tgz + yarn + + - name: Start test app in background + shell: bash + run: | + cd contrib_repo/apps/plotly_examples + yarn build + nohup npx -y serve -s build -l 3000 > output.log 2>&1 & + npx wait-on http://localhost:3000/ --timeout 300000 + + - name: Run Playwright test script + run: | + cd contrib_repo/apps/plotly_examples + npx playwright install + npx cross-env BASE_URL='http://localhost:3000/' npx playwright test tests/DeclarativeChart_v9ChartsDownload.spec.ts || true + continue-on-error: true + + - name: Zip Playwright report + shell: pwsh + run: | + cd contrib_repo/apps/plotly_examples + Compress-Archive -Path playwright-report -DestinationPath playwright-report.zip + + - name: Compute number of total tests and failures + uses: ./contrib_repo/.github/actions/playwright_metrics + with: + current_report: contrib_repo/apps/plotly_examples/playwright-report.json + baseline_report: contrib_repo/apps/plotly_examples/reports/playwright-report-v9ChartsDownload.json + + - name: Upload Playwright HTML report as artifact + uses: actions/upload-artifact@v4 + with: + name: playwright-html-report + path: contrib_repo/apps/plotly_examples/playwright-report/ + + - name: Upload Playwright JSON report as artifact + uses: actions/upload-artifact@v4 + with: + name: playwright-json-report + path: contrib_repo/apps/plotly_examples/playwright-report.json + + - name: Move Playwright JSON report to reports folder for scheduled runs + if: github.event_name == 'schedule' + shell: bash + run: | + cd contrib_repo/apps/plotly_examples + mv playwright-report.json reports/playwright-report-v9ChartsDownload.json + git add reports/playwright-report-v9ChartsDownload.json + + - name: Create branch name for scheduled report + if: github.event_name == 'schedule' + id: branch + shell: bash + run: | + BRANCH="playwright-report-v9ChartsDownload-$(date +'%Y%m%d-%H%M%S')" + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + - name: Commit and push v9 charts download scheduled report + if: github.event_name == 'schedule' + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: ${{ steps.branch.outputs.branch }} + create_branch: true + commit_message: "chore: (auto) Update v9 charts download playwright report" + file_pattern: apps/plotly_examples/reports/playwright-report-v9ChartsDownload.json + commit_user_name: github-actions[bot] + commit_user_email: github-actions[bot]@users.noreply.github.com + repository: contrib_repo + + - name: "Publish Notification for scheduled report commit" + if: github.event_name == 'schedule' && steps.auto-commit-action.outputs.changes_detected == 'true' + shell: bash + run: | + echo "v9 charts download button Playwright report committed. [Create a pull request](https://github.com/${{ github.repository }}/pull/new/${{ steps.branch.outputs.branch }})" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/update_v9ChartsDownload_snapshots.yml b/.github/workflows/update_v9ChartsDownload_snapshots.yml new file mode 100644 index 00000000000..ab6f4ce37c0 --- /dev/null +++ b/.github/workflows/update_v9ChartsDownload_snapshots.yml @@ -0,0 +1,79 @@ +name: Update v9 Charts Download test snapshots + +on: + workflow_run: + workflows: ["build and deploy to fluentchartseval"] # Name of Workflow A + types: + - completed + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + update_snapshots: + runs-on: windows-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Enable Git long paths + run: git config --global core.longpaths true + + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install packages + run: | + cd apps/plotly_examples + yarn + + - name: Skip if last commit is snapshot chore + id: skip_check + shell: bash + run: | + git fetch origin ${{ github.ref }} --depth=1 + last_msg=$(git log -1 --pretty=%B) + if [[ "$last_msg" == *"(chore) (auto) update react-charting snapshots"* || "$last_msg" == *"(chore) (auto) update react-charts snapshots"* || "$last_msg" == *"(chore) (auto) update react-charts storybook snapshots"* ]]; then + echo "skip_job=true" >> $GITHUB_OUTPUT + else + echo "skip_job=false" >> $GITHUB_OUTPUT + fi + + - name: Run Playwright update snapshots + if: github.event_name == 'workflow_dispatch' || steps.skip_check.outputs.skip_job != 'true' + run: | + cd apps/plotly_examples + npx playwright install + npx cross-env BASE_URL='https://fluentchartseval.azurewebsites.net/' npx playwright test tests/DeclarativeChart_v9ChartsDownload.spec.ts --update-snapshots || true + + - name: Create branch with timestamp + if: github.event_name == 'workflow_dispatch' || steps.skip_check.outputs.skip_job != 'true' + id: branch + shell: bash + run: | + BRANCH="update-v9ChartsDownload-snapshots-$(date +'%Y%m%d-%H%M%S')" + git checkout -b $BRANCH + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + - name: Commit changes to repo + if: github.event_name == 'workflow_dispatch' || steps.skip_check.outputs.skip_job != 'true' + id: auto-commit-action + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: ${{ steps.branch.outputs.branch }} + create_branch: true + commit_message: (chore) (auto) update react-charts snapshots + file_pattern: '*.png' + + - name: "Publish Notification regarding changes" + if: (github.event_name == 'workflow_dispatch' || steps.skip_check.outputs.skip_job != 'true') && steps.auto-commit-action.outputs.changes_detected == 'true' + shell: bash + run: | + echo "Create a pull request using this link: https://www.github.com/microsoft/fluentui-charting-contrib/pull/new/${{ steps.branch.outputs.branch }}" >> $GITHUB_STEP_SUMMARY diff --git a/apps/plotly_examples/tests/DeclarativeChart.spec.ts b/apps/plotly_examples/tests/DeclarativeChart.spec.ts index 5640cee57bc..fc9e1376e8a 100644 --- a/apps/plotly_examples/tests/DeclarativeChart.spec.ts +++ b/apps/plotly_examples/tests/DeclarativeChart.spec.ts @@ -47,9 +47,36 @@ for (const testConfig of testMatrix) { const combobox = page.getByRole('combobox'); await combobox.nth(1).click(); const listitems = listbox.last().getByRole('option'); + // Check if the index is available, if not scroll listbox to load more items + let totalOptions = await listitems.count(); + let maxAttempts = 5; // Prevent infinite loop + let attempts = 0; - await listitems.nth(index).scrollIntoViewIfNeeded(); - await listitems.nth(index).click(); + while (index >= totalOptions && attempts < maxAttempts) { + // Scroll to bottom of listbox to trigger loading more items + await listbox.last().evaluate((el) => { + el.scrollTop = el.scrollHeight; + }); + // Wait for items to load + await page.waitForTimeout(1000); + + const newTotalOptions = await listitems.count(); + if (newTotalOptions <= totalOptions) { + // No new items loaded, break to avoid infinite loop + break; + } + totalOptions = newTotalOptions; + attempts++; + } + + // Use the actual index if available, otherwise use the last available option + const actualIndex = Math.min(index, totalOptions - 1); + if (actualIndex !== index) { + console.warn(`Index ${index} not available in dropdown. Using index ${actualIndex} instead. Total available: ${totalOptions}`); + } + + await listitems.nth(actualIndex).scrollIntoViewIfNeeded(); + await listitems.nth(actualIndex).click(); const chart = page.getByTestId('chart-container'); await page.mouse.move(0, 0); // Move mouse to top-left corner if (!chartsListWithErrors.includes(index + 1)) { diff --git a/apps/plotly_examples/tests/DeclarativeChart_v9.spec.ts b/apps/plotly_examples/tests/DeclarativeChart_v9.spec.ts index ff4008c0cc0..d5c65bc3d4e 100644 --- a/apps/plotly_examples/tests/DeclarativeChart_v9.spec.ts +++ b/apps/plotly_examples/tests/DeclarativeChart_v9.spec.ts @@ -47,8 +47,36 @@ for (const testConfig of testMatrix) { const combobox = page.getByRole('combobox'); await combobox.nth(1).click(); const listitems = listbox.last().getByRole('option'); - await listitems.nth(index).scrollIntoViewIfNeeded(); - await listitems.nth(index).click(); + // Check if the index is available, if not scroll listbox to load more items + let totalOptions = await listitems.count(); + let maxAttempts = 5; // Prevent infinite loop + let attempts = 0; + + while (index >= totalOptions && attempts < maxAttempts) { + // Scroll to bottom of listbox to trigger loading more items + await listbox.last().evaluate((el) => { + el.scrollTop = el.scrollHeight; + }); + // Wait for items to load + await page.waitForTimeout(1000); + + const newTotalOptions = await listitems.count(); + if (newTotalOptions <= totalOptions) { + // No new items loaded, break to avoid infinite loop + break; + } + totalOptions = newTotalOptions; + attempts++; + } + + // Use the actual index if available, otherwise use the last available option + const actualIndex = Math.min(index, totalOptions - 1); + if (actualIndex !== index) { + console.warn(`Index ${index} not available in dropdown. Using index ${actualIndex} instead. Total available: ${totalOptions}`); + } + + await listitems.nth(actualIndex).scrollIntoViewIfNeeded(); + await listitems.nth(actualIndex).click(); const chart = page.getByTestId('chart-container-v9'); await page.mouse.move(0, 0); // Move mouse to top-left corner if (!chartsListWithErrorsV9.includes(index + 1)) { @@ -60,4 +88,4 @@ for (const testConfig of testMatrix) { }); }; }); -} +} \ No newline at end of file diff --git a/apps/plotly_examples/tests/DeclarativeChart_v9ChartsDownload.spec.ts b/apps/plotly_examples/tests/DeclarativeChart_v9ChartsDownload.spec.ts new file mode 100644 index 00000000000..103c75ddaa3 --- /dev/null +++ b/apps/plotly_examples/tests/DeclarativeChart_v9ChartsDownload.spec.ts @@ -0,0 +1,100 @@ +/* eslint-disable no-loop-func */ +import { test, expect, Page, BrowserContext } from '@playwright/test'; +import { testMatrix } from './test-matrix'; + + +for (const testConfig of testMatrix) { + const theme = testConfig.theme; + const mode = testConfig.mode; + const locale = testConfig.locale; + const testLocaleName = locale ? `-${locale}` : ''; + const highContrast = testConfig.highContrast ? '-HighContrast' : ''; + test.describe('', () => { + let context: BrowserContext; + let page: Page; + + test.beforeAll(async ({ browser }) => { + if (testConfig.highContrast) { + context = await browser.newContext({ locale, forcedColors: 'active', colorScheme: theme === 'Dark'? 'dark': 'light' }); + } + else { + context = await browser.newContext({ locale }); + } + + page = await context.newPage(); + await page.goto(process.env.BASE_URL!); + }); + + test.afterAll(async () => { + await context?.close(); + }); + for (let index = testConfig.startExampleIndex; index <= testConfig.endExampleIndex; index++) { + test(`Declarative chart example ${index + 1}-${theme}-${mode} mode${testLocaleName}${highContrast} Download V9 Chart Image`, async () => { + const iframe = page.locator('#webpack-dev-server-client-overlay'); + if (await iframe.count() > 0) { + await iframe.evaluate((el) => el.remove()).catch(() => { + console.warn("Failed to remove overlay iframe."); + }); + } + await page.getByRole('combobox').first().click(); + const listbox = page.getByRole('listbox'); + await listbox.getByRole('option').locator(`text=${theme}`).click(); + const rtlSwitch = page.getByTestId('rtl_switch'); + const isCurrentlyRTL = await rtlSwitch.isChecked(); + if ((mode === 'RTL' && !isCurrentlyRTL) || (mode === 'LTR' && isCurrentlyRTL)) { + await rtlSwitch.click(); + } + const combobox = page.getByRole('combobox'); + await combobox.nth(1).click(); + const listitems = listbox.last().getByRole('option'); + + // Check if the index is available, if not scroll listbox to load more items + let totalOptions = await listitems.count(); + let maxAttempts = 5; // Prevent infinite loop + let attempts = 0; + + while (index >= totalOptions && attempts < maxAttempts) { + // Scroll to bottom of listbox to trigger loading more items + await listbox.last().evaluate((el) => { + el.scrollTop = el.scrollHeight; + }); + // Wait for items to load + await page.waitForTimeout(1000); + + const newTotalOptions = await listitems.count(); + if (newTotalOptions <= totalOptions) { + // No new items loaded, break to avoid infinite loop + break; + } + totalOptions = newTotalOptions; + attempts++; + } + + // Use the actual index if available, otherwise use the last available option + const actualIndex = Math.min(index, totalOptions - 1); + if (actualIndex !== index) { + console.warn(`Index ${index} not available in dropdown. Using index ${actualIndex} instead. Total available: ${totalOptions}`); + } + + await listitems.nth(actualIndex).scrollIntoViewIfNeeded(); + await listitems.nth(actualIndex).click(); + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.getByRole('button', { name: "Download V9 Chart as Image" }).click() + ]); + const downloadedImageBuffer = await stream2buffer(await download.createReadStream()) + expect(downloadedImageBuffer).toMatchSnapshot(`downloaded-declarative-chart-example-${index + 1}-${theme}-${mode}mode${testLocaleName}${highContrast}.png`) + await download.delete() + }); + }; + }); +} + +async function stream2buffer(stream: any) { + return new Promise((resolve, reject) => { + const _buf = Array(); + stream.on("data", (chunk: any) => _buf.push(chunk)); + stream.on("end", () => resolve(Buffer.concat(_buf))); + stream.on("error", (err: any) => reject(`error converting stream - ${err}`)); + }); +} \ No newline at end of file