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' });