diff --git a/.github/workflows/blackduck.yml b/.github/workflows/blackduck.yml index eac581a..87c9909 100644 --- a/.github/workflows/blackduck.yml +++ b/.github/workflows/blackduck.yml @@ -7,11 +7,32 @@ on: jobs: blackduck: - name: Blackduck scan - uses: Fisker-Inc/github-actions/.github/workflows/blackduck.yml@main - with: - project: ota-admin-portal - secrets: - github-token: ${{ secrets.GITHUB_TOKEN }} - blackduck-url: ${{ secrets.BLACKDUCK_URL }} - blackduck-api-token: ${{ secrets.BLACKDUCK_API_KEY }} \ No newline at end of file + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '16' + cache: npm + + - run: npm install + - run: npm run build + + - name: Run Synopsys Detect INTELLIGENT + run: | + bash <(curl -s -L https://detect.synopsys.com/detect8.sh) \ + --blackduck.url=${{ secrets.BLACKDUCK_URL }} \ + --blackduck.api.token=${{ secrets.BLACKDUCK_API_KEY }} \ + --blackduck.trust.cert=true \ + --detect.project.version.update=true \ + --detect.project.name='ota-admin-portal' \ + --detect.excluded.directories='node_modules' \ + --detect.project.version.name=$GITHUB_REF_NAME \ + --detect.blackduck.scan.mode="INTELLIGENT" \ + --detect.detector.search.depth=3 \ + --detect.detector.search.continue=true \ + --detect.npm.include.dev.dependencies=false + # --detect.policy.check.fail.on.severities=ALL,NONE,UNSPECIFIED,TRIVIAL,MINOR,MAJOR,CRITICAL,BLOCKER - Use it if you want to fail the build on a certain severity type + # --detect.detector.search.continue=true - If true, the bom tool search will continue to look for nested bom tools of the same type to the maximum search depth \ No newline at end of file diff --git a/.github/workflows/blackduck_rapid.yml b/.github/workflows/blackduck_rapid.yml new file mode 100644 index 0000000..df2b1be --- /dev/null +++ b/.github/workflows/blackduck_rapid.yml @@ -0,0 +1,39 @@ +name: Blackduck Rapid scan + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +jobs: + blackduck: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '16' + cache: npm + + - run: npm install + - run: npm run build + + - name: Run Synopsys Detect RAPID + run: | + bash <(curl -s -L https://detect.synopsys.com/detect8.sh) \ + --blackduck.url=${{ secrets.BLACKDUCK_URL }} \ + --blackduck.api.token=${{ secrets.BLACKDUCK_API_KEY }} \ + --blackduck.trust.cert=true \ + --detect.project.version.update=true \ + --detect.project.name='ota-admin-portal' \ + --detect.excluded.directories='node_modules' \ + --detect.project.version.name=$GITHUB_REF_NAME \ + --detect.blackduck.scan.mode="RAPID" \ + --detect.detector.search.depth=3 \ + --detect.detector.search.continue=true \ + --detect.npm.include.dev.dependencies=false + # --detect.detector.search.continue=true - If true, the bom tool search will continue to look for nested bom tools of the same type to the maximum search depth \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 87e430a..da1ced7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,10 +3,7 @@ name: OTA Portal Deploy on: push: branches: - - develop - main - - "release/**" - - "hotfix/**" env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} @@ -19,10 +16,9 @@ env: REGISTRY: fiskercloud.azurecr.io jobs: - build: + build-dev: runs-on: ubuntu-latest - outputs: - build-env: ${{ steps.set-env.outputs.ENVIRONMENT }} + steps: - name: Slack Notification uses: rtCamp/action-slack-notify@v2 @@ -42,64 +38,30 @@ jobs: username: ${{ secrets.AZURE_CLIENT_ID }} password: ${{ secrets.AZURE_CLIENT_SECRET }} - - name: Set Env - id: set-env - run: | - case ${GITHUB_REF} in - refs/heads/develop) - ENVIRONMENT=dev;; - refs/heads/release/*) - ENVIRONMENT=stg;; - refs/heads/hotfix/*) - ENVIRONMENT=stg;; - refs/heads/main) - ENVIRONMENT=prd;; - *) - ENVIRONMENT=dev;; - esac - echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_ENV - echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_OUTPUT - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Build and push - uses: docker/build-push-action@v3 + - name: Build and push DEV + uses: docker/build-push-action@v4 with: context: . - build-args: ENVIRONMENT=${{ env.ENVIRONMENT }} + build-args: ENVIRONMENT=dev push: true - tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-${{ env.ENVIRONMENT }} + tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-dev cache-from: type=gha cache-to: type=gha,mode=max - - - name: Build and push new prod - if: github.ref == 'refs/heads/main' - uses: docker/build-push-action@v3 - with: - context: . - build-args: ENVIRONMENT=cec-${{ env.ENVIRONMENT }} - push: true - tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-cec-${{ env.ENVIRONMENT }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push Germany - if: github.ref == 'refs/heads/main' - uses: docker/build-push-action@v3 - with: - context: . - build-args: ENVIRONMENT=cec-eu${{ env.ENVIRONMENT }} - push: true - tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-cec-eu${{ env.ENVIRONMENT }} - cache-from: type=gha - cache-to: type=gha,mode=max - - deploy: - needs: build + + - name: Notify if failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to build ${{ env.PROJECT }} dev! :this-is-fine:" + + deploy-dev: + needs: [build-dev] runs-on: [self-hosted, azure] - env: - ENVIRONMENT: ${{ needs.build.outputs.build-env }} + environment: dev steps: - name: Checkout uses: actions/checkout@v3 @@ -107,60 +69,336 @@ jobs: - uses: rtCamp/action-slack-notify@v2 env: MSG_MINIMAL: true - SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to ${{ env.ENVIRONMENT }}... :partydeploy:" + SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to dev... :partydeploy:" - - name: Deploy + - name: Deploy to dev run: |- helm upgrade \ - --kube-context $ENVIRONMENT \ + --kube-context dev \ --set image.registry=$REGISTRY \ --set image.name=$PROJECT \ - --set image.tag=$TAG-$ENVIRONMENT \ - --wait -i -f k8s/values-$ENVIRONMENT.yaml $PROJECT k8s/ + --set image.tag=$TAG-dev \ + --wait -i -f k8s/values-dev.yaml $PROJECT k8s/ - - name: Notify deploy + - name: Notify deploy failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to deploy ${{ env.PROJECT }} on dev! :this-is-fine:" + + - name: Notify deploy success uses: rtCamp/action-slack-notify@v2 env: MSG_MINIMAL: true - SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to ${{ env.ENVIRONMENT }}! :gopher_party:" + SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to dev! :gopher_party:" - - name: Deploy new prod - if: github.ref == 'refs/heads/main' - run: |- - helm upgrade \ - --kube-context cec-$ENVIRONMENT-cluster-1 \ - --set image.registry=$REGISTRY \ - --set image.name=$PROJECT \ - --set image.tag=$TAG-cec-$ENVIRONMENT \ - --wait -i -f k8s/values-cec-$ENVIRONMENT.yaml $PROJECT k8s/ - - - name: Notify deploy new - if: github.ref == 'refs/heads/main' + build-stg: + runs-on: ubuntu-latest + needs: [build-dev, deploy-dev] + steps: + - name: Slack Notification uses: rtCamp/action-slack-notify@v2 - env: - MSG_MINIMAL: true - SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to cec-${{ env.ENVIRONMENT }}! :gopher_party:" - - name: Deploy Germany - if: github.ref == 'refs/heads/main' - run: |- - helm upgrade \ - --kube-context cec-eu$ENVIRONMENT-cluster-1 \ - --set image.registry=$REGISTRY \ - --set image.name=$PROJECT \ - --set image.tag=$TAG-cec-eu$ENVIRONMENT \ - --wait -i -f k8s/values-cec-eu$ENVIRONMENT.yaml $PROJECT k8s/ + - name: Checkout + uses: actions/checkout@v3 - - name: Notify deploy Germany - if: github.ref == 'refs/heads/main' - uses: rtCamp/action-slack-notify@v2 - env: - MSG_MINIMAL: true - SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to cec-eu${{ env.ENVIRONMENT }}! :gopher_party:" + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to ACR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.AZURE_CLIENT_ID }} + password: ${{ secrets.AZURE_CLIENT_SECRET }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push STG + uses: docker/build-push-action@v4 + with: + context: . + build-args: ENVIRONMENT=stg + push: true + tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-stg + cache-from: type=gha + cache-to: type=gha,mode=max - name: Notify if failure if: ${{ failure() }} uses: rtCamp/action-slack-notify@v2 env: SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Failed to deploy ${{ env.PROJECT }} to ${{ env.ENVIRONMENT }}! :this-is-fine:" + SLACK_MESSAGE: "Failed to build ${{ env.PROJECT }} stg! :this-is-fine:" + + + deploy-stg: + needs: [build-dev, deploy-dev, build-stg] + runs-on: [self-hosted, azure] + environment: stg + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to stg... :partydeploy:" + + - name: Deploy to stg + run: |- + helm upgrade \ + --kube-context stg \ + --set image.registry=$REGISTRY \ + --set image.name=$PROJECT \ + --set image.tag=$TAG-stg \ + --wait -i -f k8s/values-stg.yaml $PROJECT k8s/ + + - name: Notify deploy failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to deploy ${{ env.PROJECT }} on stg! :this-is-fine:" + + - name: Notify deploy success + uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to stg! :gopher_party:" + + build-preprod: + runs-on: ubuntu-latest + needs: [build-dev, deploy-dev] + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + + - name: Checkout + uses: actions/checkout@v3 + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to ACR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.AZURE_CLIENT_ID }} + password: ${{ secrets.AZURE_CLIENT_SECRET }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push PREPROD + uses: docker/build-push-action@v4 + with: + context: . + build-args: ENVIRONMENT=prd + push: true + tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-prd + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Notify if failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to build ${{ env.PROJECT }} preprod! :this-is-fine:" + + deploy-preprod: + needs: [build-dev, deploy-dev, build-preprod] + runs-on: [self-hosted, azure] + environment: stg + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to preprod... :partydeploy:" + + - name: Deploy to preprod + run: |- + helm upgrade \ + --kube-context prd \ + --set image.registry=$REGISTRY \ + --set image.name=$PROJECT \ + --set image.tag=$TAG-prd \ + --wait -i -f k8s/values-prd.yaml $PROJECT k8s/ + + - name: Notify deploy failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to deploy ${{ env.PROJECT }} on preprod! :this-is-fine:" + + - name: Notify deploy success + uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to preprod! :gopher_party:" + + build-cec-prd: + runs-on: ubuntu-latest + needs: [build-dev, deploy-dev, build-stg, deploy-stg, build-preprod, deploy-preprod] + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + + - name: Checkout + uses: actions/checkout@v3 + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to ACR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.AZURE_CLIENT_ID }} + password: ${{ secrets.AZURE_CLIENT_SECRET }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push CEC-PRD + uses: docker/build-push-action@v4 + with: + context: . + build-args: ENVIRONMENT=cec-prd + push: true + tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-cec-prd + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Notify if failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to build ${{ env.PROJECT }} cec-prd! :this-is-fine:" + + deploy-cec-prd: + needs: [build-dev, deploy-dev, build-stg, deploy-stg, build-preprod, deploy-preprod, build-cec-prd] + runs-on: [self-hosted, azure] + environment: prd + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to cec-prd... :partydeploy:" + + - name: Deploy to cec-prd + run: |- + helm upgrade \ + --kube-context cec-prd-cluster-1 \ + --set image.registry=$REGISTRY \ + --set image.name=$PROJECT \ + --set image.tag=$TAG-cec-prd \ + --wait -i -f k8s/values-cec-prd.yaml $PROJECT k8s/ + + - name: Notify deploy failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to deploy ${{ env.PROJECT }} on cec-prd! :this-is-fine:" + + - name: Notify deploy success + uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to cec-prd! :gopher_party:" + + build-cec-euprd: + runs-on: ubuntu-latest + needs: [build-dev, deploy-dev, build-stg, deploy-stg, build-preprod, deploy-preprod] + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + + - name: Checkout + uses: actions/checkout@v3 + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to ACR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.AZURE_CLIENT_ID }} + password: ${{ secrets.AZURE_CLIENT_SECRET }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push CEC-EUPRD + uses: docker/build-push-action@v4 + with: + context: . + build-args: ENVIRONMENT=cec-euprd + push: true + tags: ${{ env.REGISTRY }}/${{ env.PROJECT }}:${{ env.TAG }}-cec-euprd + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Notify if failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to build ${{ env.PROJECT }} cec-euprd! :this-is-fine:" + + + deploy-cec-euprd: + needs: [build-dev, deploy-dev, build-stg, deploy-stg, build-preprod, deploy-preprod, build-cec-euprd] + runs-on: [self-hosted, azure] + environment: prd + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Deploying ${{ env.PROJECT }} to cec-euprd... :partydeploy:" + + - name: Deploy to cec-euprd + run: |- + helm upgrade \ + --kube-context cec-euprd-cluster-1 \ + --set image.registry=$REGISTRY \ + --set image.name=$PROJECT \ + --set image.tag=$TAG-cec-euprd \ + --wait -i -f k8s/values-cec-euprd.yaml $PROJECT k8s/ + + - name: Notify deploy failure + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Failed to deploy ${{ env.PROJECT }} on cec-euprd! :this-is-fine:" + + - name: Notify deploy success + uses: rtCamp/action-slack-notify@v2 + env: + MSG_MINIMAL: true + SLACK_MESSAGE: "Successfully deployed ${{ env.PROJECT }} to cec-euprd! :gopher_party:" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd2e6ee..0291931 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: Node.js CI on: push: branches: - - develop + - main pull_request: jobs: diff --git a/src/components/Contexts/CarUpdatesContext.jsx b/src/components/Contexts/CarUpdatesContext.jsx index b4e6ed5..1ce0bde 100644 --- a/src/components/Contexts/CarUpdatesContext.jsx +++ b/src/components/Contexts/CarUpdatesContext.jsx @@ -127,8 +127,16 @@ export const CarUpdatesProvider = ({ children }) => { }; const getDownloadProgress = (status) => { - if (status.package_total > 0) + const disabled = status.status === "install_succeeded"; + if (disabled) { + return -1; + } + + const calculated = status.package_total > 0; + if (calculated) { return Math.floor((100 * status.package_current) / status.package_total); + } + return 0; }; diff --git a/src/components/Manifest/List/index.jsx b/src/components/Manifest/List/index.jsx index da6dd49..d1864d2 100644 --- a/src/components/Manifest/List/index.jsx +++ b/src/components/Manifest/List/index.jsx @@ -274,12 +274,10 @@ const MainForm = () => { const handleSelect = (event, manifest) => { setUpdateManifestIds((selected) => { - if (event.target.checked && selected.find((id) => id === manifest.id)) { - return selected; - } else if (event.target.checked) { + if (event.target.checked) { return [...selected, manifest.id]; } - return selected.filter(({ id }) => id !== manifest.id); + return selected.filter(id => id !== manifest.id); }); }; diff --git a/src/components/Manifest/List/index.test.jsx b/src/components/Manifest/List/index.test.jsx index e7f2ab4..e821d67 100644 --- a/src/components/Manifest/List/index.test.jsx +++ b/src/components/Manifest/List/index.test.jsx @@ -3,6 +3,7 @@ jest.mock("../../Contexts/UserContext"); import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; +import userEvent from '@testing-library/user-event'; import { BrowserRouter } from "react-router-dom"; import { UserProvider, setToken } from "../../Contexts/UserContext"; @@ -32,4 +33,14 @@ describe("Manifest List Component", () => { fireEvent.click(screen.getByText("Archived")); expect(archiveActionEl.innerHTML).toBe("Activate"); }); + + it("properly selects and deselects a manifest", () => { + const { queryAllByRole } = render(Page); + const checkbox = queryAllByRole("checkbox")[0]; + expect(checkbox).not.toBeChecked(); + userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + userEvent.click(checkbox); + expect(checkbox).not.toBeChecked(); + }) }); diff --git a/src/utils/taskRunner.js b/src/utils/taskRunner.js index b13ccec..78fcd9f 100644 --- a/src/utils/taskRunner.js +++ b/src/utils/taskRunner.js @@ -8,7 +8,7 @@ export default class TaskRunner { if (total) { this._total = total; - this._responses = new Array(total); + this._responses = new Array(total).fill(undefined); } this._onComplete = new Promise((resolve, reject) => { @@ -18,7 +18,8 @@ export default class TaskRunner { } execute() { - if (this._running >= this._concurrencyLimit || this._queue.length === 0) { + const isBusy = this._running >= this._concurrencyLimit || this._queue.length === 0; + if (isBusy || this._paused) { return; } @@ -38,10 +39,16 @@ export default class TaskRunner { } resolve(response); } catch (error) { + if (this._responses) { + this._responses[index] = new Error(`Task was rejected: ${error}`); + } reject(error); } finally { this._running -= 1; - this.#progress(); + this._complete += 1; + if (this._complete === this._total) { + this._onCompleteResolve(this._responses); + } this.execute(); } } @@ -51,17 +58,15 @@ export default class TaskRunner { }); } - #progress() { - this._complete += 1; - if (this._complete === this._total) { - this._onCompleteResolve(this._responses); - } - } - async onComplete() { if (!this._total) { this._onCompleteReject(new Error("Total is required to determine onComplete.")); } return this._onComplete; } + + cancel() { + this._concurrencyLimit = 0; + this._onCompleteReject(this._responses); + } } \ No newline at end of file diff --git a/src/utils/taskRunner.test.js b/src/utils/taskRunner.test.js index 37749c3..1d5589f 100644 --- a/src/utils/taskRunner.test.js +++ b/src/utils/taskRunner.test.js @@ -4,6 +4,9 @@ const mockPromise = async (id, ms) => { await new Promise(resolve => setTimeout(resolve, ms)); return id; } +const mockPromiseReject = async (id, ms) => { + return new Promise((_, reject) => setTimeout(reject(id), ms)); +} const mockPromiseError = async (id, ms) => { await new Promise(resolve => setTimeout(resolve, ms)); return new Error(`Task ${id} had an error`); @@ -44,19 +47,10 @@ describe("TaskRunner", () => { it("runs tasks in order", async () => { const actual = []; const taskRunner = new TaskRunner(2); - taskRunner.push(asyncFn1) - .then((id) => { - actual.push(id); - }); - taskRunner.push(asyncFn2) - .then((id) => { - actual.push(id); - }); - taskRunner.push(asyncFn3) - .then((id) => { - actual.push(id); - }); - await new Promise(resolve => setTimeout(resolve, 500)); + taskRunner.push(asyncFn1).then(id => actual.push(id)); + taskRunner.push(asyncFn2).then(id => actual.push(id)); + taskRunner.push(asyncFn3).then(id => actual.push(id)); + await new Promise(resolve => setTimeout(resolve, 200)); expect(actual).toEqual([2, 3, 1]); }); @@ -72,6 +66,20 @@ describe("TaskRunner", () => { }); }); + it("resolves a promise when all tasks are complete, even if some are rejected", async () => { + const error = new Error(`Task was rejected: 3`); + const taskRunner = new TaskRunner(2, 5); + taskRunner.push(() => mockPromise(1, 600)); + taskRunner.push(() => mockPromise(2, 300)); + taskRunner.push(() => mockPromiseReject(3, 200)).catch(payload => expect(payload).toBe(3)); + taskRunner.push(() => mockPromise(4, 600)); + taskRunner.push(() => mockPromise(5, 100)); + + await taskRunner.onComplete().then((response) => { + expect(response).toStrictEqual([1, 2, error, 4, 5]); + }); + }); + it("resolves a promise when all tasks are complete, even if some fail", async () => { const error = new Error(`Task 3 had an error`); const taskRunner = new TaskRunner(2, 5); @@ -85,7 +93,7 @@ describe("TaskRunner", () => { }); }); - it("rejects a promise when the total number of tasks is unknown", async () => { + it("immediately rejects onComplete when the total number of tasks is unknown", async () => { const taskRunner = new TaskRunner(2); taskRunner.push(() => mockPromise(1, 600)); taskRunner.push(() => mockPromise(2, 300)); @@ -96,4 +104,21 @@ describe("TaskRunner", () => { expect(error.message).toBe("Total is required to determine onComplete."); }); }); + + it("cancels a task runner and returns progress", async () => { + const taskRunner = new TaskRunner(2, 5); + taskRunner.push(() => mockPromise(1, 600)); + taskRunner.push(() => mockPromise(2, 300)); + taskRunner.push(() => mockPromise(3, 200)); + taskRunner.push(() => mockPromise(4, 600)); + taskRunner.push(() => mockPromise(5, 100)); + + setTimeout(() => { + taskRunner.cancel(); + }, 550); + + await taskRunner.onComplete().catch((response) => { + expect(response).toStrictEqual([undefined, 2, 3, undefined, undefined]); + }); + }); });