diff --git a/.github/workflows/automated-tests-publish-results.yml b/.github/workflows/automated-tests-publish-results.yml deleted file mode 100644 index 08a4f6a8..00000000 --- a/.github/workflows/automated-tests-publish-results.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Automated tests - publish results -on: - workflow_run: - workflows: - - Automated tests - types: - - completed - -jobs: - get-pr-data: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.event == 'pull_request' }} - outputs: - pr-number: ${{ steps.set-env.outputs.pr-number }} - workflow-id: ${{ steps.set-env.outputs.workflow-id }} - steps: - # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow - - name: Download artifact - uses: actions/github-script@v6 - with: - script: | - let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - - let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name == "pr-comment-data" - })[0]; - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - let fs = require('fs'); - fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr-comment-data.zip`, Buffer.from(download.data)); - - name: Unzip artifact - run: unzip pr-comment-data.zip - - name: Set env variables - id: set-env - run: | - echo "pr-number=$(cat ./pr_number)" >> $GITHUB_OUTPUT - echo "workflow-id=$(cat ./workflow_id)" >> $GITHUB_OUTPUT - comment-pr: - runs-on: ubuntu-latest - permissions: - pull-requests: write - needs: get-pr-data - steps: - - name: Find Comment - uses: peter-evans/find-comment@v2 - id: fc - with: - issue-number: ${{ needs.get-pr-data.outputs.pr-number }} - comment-author: "github-actions[bot]" - body-includes: Automated tests Summary - - name: Remove previous comment - if: steps.fc.outputs.comment-id != '' - uses: actions/github-script@v6 - with: - script: | - github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: ${{ steps.fc.outputs.comment-id }} - }) - - name: Passing tests comment - if: ${{ github.event.workflow_run.conclusion == 'success' }} - uses: peter-evans/create-or-update-comment@v2 - with: - issue-number: ${{ needs.get-pr-data.outputs.pr-number }} - body: | -

Automated tests Summary

-

:white_check_mark: All the CI tests have passed!

- - name: Failing tests comment - if: ${{ github.event.workflow_run.conclusion == 'failure' }} - uses: peter-evans/create-or-update-comment@v2 - with: - issue-number: ${{ needs.get-pr-data.outputs.pr-number }} - body: | -

Automated tests Summary

-

:rotating_light: Test workflow has failed

- - ___ - - [Click here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ needs.get-pr-data.outputs.workflow-id }}) to check the action test reports diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..1b832c26 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,27 @@ +name: Unit tests +on: [pull_request] + +permissions: + contents: read + +jobs: + unit-tests: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Install dependencies + run: npm install + - name: Run unit tests + run: npm run test:unit:coverage + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ diff --git a/.gitignore b/.gitignore index d7d91506..08b5f0c2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ dist/ dist-tsc/ .env +# vitest +coverage/ + # playwright test-results/ playwright-report/ diff --git a/src/core/api/BbbPluginSdk.ts b/src/core/api/BbbPluginSdk.ts index 82a09959..9f0c1d47 100644 --- a/src/core/api/BbbPluginSdk.ts +++ b/src/core/api/BbbPluginSdk.ts @@ -63,6 +63,8 @@ import { useCustomMutation } from '../../data-creation/hook'; import { UseCustomMutationFunction } from '../../data-creation/types'; import { UseMeetingDataFunction } from '../../data-consumption/domain/meeting/meeting-data/types'; import { useMeetingData } from '../../data-consumption/domain/meeting/meeting-data/hooks'; +import { UseTimerFunction } from '../../data-consumption/domain/timer/types'; +import { useTimer } from '../../data-consumption/domain/timer/hooks'; declare const window: PluginBrowserWindow; @@ -108,6 +110,7 @@ export abstract class BbbPluginSdk { pluginApi.useMeetingData = useMeetingData as UseMeetingDataFunction; pluginApi.useUsersBasicInfo = (() => useUsersBasicInfo()) as UseUsersBasicInfoFunction; pluginApi.useTalkingIndicator = (() => useTalkingIndicator()) as UseTalkingIndicatorFunction; + pluginApi.useTimer = (() => useTimer()) as UseTimerFunction; pluginApi.getJoinUrl = (params) => getJoinUrl(params); pluginApi.useLoadedChatMessages = ( () => useLoadedChatMessages()) as UseLoadedChatMessagesFunction; diff --git a/src/core/api/types.ts b/src/core/api/types.ts index fdf0fdee..ab9652e5 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -39,6 +39,7 @@ import { GetUiDataFunction } from '../../ui-data/getters/types'; import { UseCustomQueryFunction } from '../../data-consumption/domain/shared/custom-query/types'; import { UseCustomMutationFunction } from '../../data-creation/types'; import { UseMeetingDataFunction } from '../../data-consumption/domain/meeting/meeting-data/types'; +import { UseTimerFunction } from '../../data-consumption/domain/timer/types'; // Setter Functions for the API export type SetPresentationToolbarItems = (presentationToolbarItem: @@ -200,6 +201,13 @@ export interface PluginApi { * */ useTalkingIndicator?: UseTalkingIndicatorFunction; + /** + * Returns an object containing the current state of the meeting timer. + * + * @returns `GraphqlResponseWrapper` with the TimerData type. + * + */ + useTimer?: UseTimerFunction; /** * Returns a boolean telling if the plugin should be unmounted or not based on the mounting * of the meeting. This means that if the meeting end or the user is ejected, this will diff --git a/src/data-consumption/domain/timer/hooks.ts b/src/data-consumption/domain/timer/hooks.ts new file mode 100644 index 00000000..aede873a --- /dev/null +++ b/src/data-consumption/domain/timer/hooks.ts @@ -0,0 +1,7 @@ +import { DataConsumptionHooks } from '../../enums'; +import { createDataConsumptionHook } from '../../factory/hookCreator'; +import { TimerData } from './types'; + +export const useTimer = () => createDataConsumptionHook( + DataConsumptionHooks.TIMER, +); diff --git a/src/data-consumption/domain/timer/index.ts b/src/data-consumption/domain/timer/index.ts new file mode 100644 index 00000000..af294047 --- /dev/null +++ b/src/data-consumption/domain/timer/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './hooks'; diff --git a/src/data-consumption/domain/timer/types.ts b/src/data-consumption/domain/timer/types.ts new file mode 100644 index 00000000..dd377bc6 --- /dev/null +++ b/src/data-consumption/domain/timer/types.ts @@ -0,0 +1,15 @@ +import { GraphqlResponseWrapper } from '../../../core'; + +export interface TimerData { + accumulated: number; + active: boolean; + songTrack: string; + time: number; + stopwatch: boolean; + running: boolean; + startedAt: string | null; + elapsed: boolean; + timePassed: number; +} + +export type UseTimerFunction = () => GraphqlResponseWrapper; diff --git a/src/data-consumption/enums.ts b/src/data-consumption/enums.ts index 238ce3c1..ea2dc29c 100644 --- a/src/data-consumption/enums.ts +++ b/src/data-consumption/enums.ts @@ -9,4 +9,5 @@ export enum DataConsumptionHooks { TALKING_INDICATOR = 'Hooks::UseTalkingIndicator', CUSTOM_SUBSCRIPTION = 'Hooks::CustomSubscription', CUSTOM_QUERY = 'Hooks::CustomQuery', + TIMER = 'Hooks::UseTimer', } diff --git a/src/data-consumption/factory/hookCreator.ts b/src/data-consumption/factory/hookCreator.ts index cf2ecfe7..eb292aef 100644 --- a/src/data-consumption/factory/hookCreator.ts +++ b/src/data-consumption/factory/hookCreator.ts @@ -37,7 +37,9 @@ export const createDataConsumptionHook = ( sortedStringify(hookArguments?.variables) !== sortedStringify(variablesState) ) { - setVariablesState(() => JSON.parse(sortedStringify(hookArguments?.variables))); + setVariablesState(() => (hookArguments?.variables === undefined + ? undefined + : JSON.parse(sortedStringify(hookArguments.variables)))); } } diff --git a/src/data-consumption/index.ts b/src/data-consumption/index.ts index 410f4af5..90af3803 100644 --- a/src/data-consumption/index.ts +++ b/src/data-consumption/index.ts @@ -4,5 +4,6 @@ export * from './domain/meeting'; export * from './domain/users'; export * from './domain/user-voice'; export * from './domain/shared'; +export * from './domain/timer'; export * from './factory/types'; diff --git a/tests/unit/data-consumption/factory/hookCreator.test.ts b/tests/unit/data-consumption/factory/hookCreator.test.ts index 404a70aa..7ace4ddb 100644 --- a/tests/unit/data-consumption/factory/hookCreator.test.ts +++ b/tests/unit/data-consumption/factory/hookCreator.test.ts @@ -116,6 +116,23 @@ describe('createDataConsumptionHook', () => { expect(variablesOf(subscribeEvents[1])).toEqual(PT_BR_VARIABLES); }); + it('resubscribes without variables when they become undefined', () => { + const { rerender } = renderHook( + ({ variables }: { variables?: object }) => createDataConsumptionHook( + DataConsumptionHooks.CUSTOM_SUBSCRIPTION, + { query: QUERY_WITH_ONE_VARIABLE, variables }, + ), + { initialProps: { variables: EN_VARIABLES as object | undefined } }, + ); + + rerender({ variables: undefined }); + + expect(unsubscribeEvents).toHaveLength(1); + expect(variablesOf(unsubscribeEvents[0])).toEqual(EN_VARIABLES); + expect(subscribeEvents).toHaveLength(2); + expect(variablesOf(subscribeEvents[1])).toBeUndefined(); + }); + it('does not resubscribe when a rerender passes a new variables object with the same content', () => { const { rerender } = renderCustomSubscriptionHook({ locale: 'en' });