Parcourir la source

Merge remote-tracking branch 'origin/feature/structure_cmf_licence_page_rebased' into develop

Olivier Massot il y a 3 ans
Parent
commit
217e5a85f0

+ 5 - 0
.env.local

@@ -17,3 +17,8 @@ CLIENT_ADMINLEG_BASE_URL=https://local.admin.opentalent.fr/#
 # Typo3 Base Url
 SSR_TYPO3_BASE_URL=https://local.sub.opentalent.fr/###subDomain###
 CLIENT_TYPO3_BASE_URL=https://local.sub.opentalent.fr/###subDomain###
+
+# Mercure push events
+MERCURE_URL=https://local.mercure.opentalent.fr/.well-known/mercure
+MERCURE_PUBLIC_URL=https://local.mercure.opentalent.fr/.well-known/mercure
+MERCURE_SUBSCRIBER_JWT_KEY=NQEupdREijrfYvCmF2mnvZQFL9zLKDH9RCYter6tUWzjemPqzicffhc2fSf0yEmM

+ 1 - 1
assets/css/.gitignore

@@ -1,2 +1,2 @@
 *.css
-*.css.map
+*.map

+ 39 - 0
components/Form/Export/LicenceCmfOrganizationER.vue

@@ -0,0 +1,39 @@
+<template>
+  <main>
+    <LayoutContainer>
+      <v-card class="mb-5">
+        <v-form
+          ref="form"
+          lazy-validation
+        >
+          <v-btn class="mr-4 ot_green ot_white--text" @click="">
+            {{ $t('generate') }}
+          </v-btn>
+        </v-form>
+      </v-card>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent } from '@nuxtjs/composition-api'
+import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
+import { Query, Model } from '@vuex-orm/core'
+import { repositoryHelper } from '~/services/store/repository'
+import {LicenceCmfOrganizationER} from "~/models/Export/LicenceCmfOrganizationER";
+
+export default defineComponent({
+  setup() {
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(LicenceCmfOrganizationER)
+    const query: Query = repository.query()
+
+    return {
+      model: LicenceCmfOrganizationER,
+      query,
+    }
+  },
+  beforeDestroy: function () {
+    repositoryHelper.cleanRepository(LicenceCmfOrganizationER)
+  }
+})
+</script>

+ 0 - 0
components/Layout/Loading.vue → components/Layout/LoadingScreen.vue


+ 29 - 0
components/Ui/Loading.vue

@@ -0,0 +1,29 @@
+<!-- Animation circulaire à afficher durant les chargements -->
+
+<template>
+  <v-progress-circular
+    indeterminate
+    :size="size"
+  />
+</template>
+
+<script lang="ts">
+import { defineComponent, toRefs } from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    size: {
+      type: Number,
+      required: false,
+      default: 32
+    }
+  },
+  setup (props) {
+    const { size } = toRefs(props)
+
+    return {
+      size
+    }
+  }
+})
+</script>

+ 1 - 1
composables/layout/Menus/configurationMenu.ts

@@ -35,7 +35,7 @@ class ConfigurationMenu extends BaseMenu implements Menu {
     }
 
     if (this.$ability.can('display', 'cmf_licence_page')) {
-      children.push(this.constructMenu('cmf_licence_generate', undefined, '/licence_cmf/organization', true))
+      children.push(this.constructMenu('cmf_licence_generate', undefined, '/cmf_licence/organization'))
     }
 
     if (this.$ability.can('display', 'parameters_page')) {

+ 1 - 1
config/nuxtConfig/build.js

@@ -7,7 +7,7 @@ export default {
   // Auto import components (https://go.nuxtjs.dev/config-components)
   components: true,
 
-  loading: '~/components/Layout/Loading.vue',
+  loading: '~/components/Layout/LoadingScreen.vue',
 
   // Build Configuration (https://go.nuxtjs.dev/config-build)
   build: {

+ 4 - 2
config/nuxtConfig/env.js

@@ -22,7 +22,8 @@ export default {
     },
     baseURL_Legacy: process.env.CLIENT_APILEG_BASE_URL,
     baseURL_adminLegacy: process.env.CLIENT_ADMINLEG_BASE_URL,
-    baseURL_typo3: process.env.CLIENT_TYPO3_BASE_URL
+    baseURL_typo3: process.env.CLIENT_TYPO3_BASE_URL,
+    baseUrl_mercure: process.env.MERCURE_URL
   },
   privateRuntimeConfig: {
     http: {
@@ -34,6 +35,7 @@ export default {
     },
     baseURL_Legacy: process.env.SSR_APILEG_BASE_URL,
     baseURL_adminLegacy: process.env.SSR_ADMINLEG_BASE_URL,
-    baseURL_typo3: process.env.SSR_TYPO3_BASE_URL
+    baseURL_typo3: process.env.SSR_TYPO3_BASE_URL,
+    baseUrl_mercure: process.env.MERCURE_URL
   }
 }

+ 1 - 0
config/nuxtConfig/plugins.js

@@ -10,6 +10,7 @@ export default {
     '~/plugins/Data/dataProvider',
     '~/plugins/Data/dataDeleter',
     '~/plugins/vuexOrm.js',
+    { src: '~/plugins/sse.ts', mode: 'client' },
     '~/plugins/phone-input',
     '~/plugins/directives.js'
   ]

+ 0 - 2
jest.config.js

@@ -20,11 +20,9 @@ module.exports = {
   },
   collectCoverage: true,
   collectCoverageFrom: [
-    '<rootDir>/components/**/*.vue',
     '<rootDir>/middleware/**/*.ts',
     '<rootDir>/services/**/*.ts',
     '<rootDir>/composables/**/*.ts',
-    '<rootDir>/pages/**/*.vue'
   ],
   setupFiles: ['<rootDir>/tests/unit/index.ts']
 }

+ 3 - 1
lang/form/fr-FR.js

@@ -21,6 +21,8 @@ export default (context, locale) => {
     updateMap: 'Mise à jour de la carte',
     start_your_research: 'Commencer à écrire pour rechercher...',
     no_coordinate_corresponding: 'Aucune coordonnées GPS ne correspondent à votre adresse',
-    quit_without_saving_warning: 'Vous souhaitez quitter ce formulaire sans avoir enregistré'
+    quit_without_saving_warning: 'Vous souhaitez quitter ce formulaire sans avoir enregistré',
+    please_wait: 'Veuillez patienter',
+    download: 'Télécharger'
   })
 }

+ 4 - 0
lang/layout/fr-FR.js

@@ -146,6 +146,10 @@ export default (context, locale) => {
     configuration: 'Configuration',
     organization_page: 'Fiche de la structure',
     cmf_licence_generate: 'Générer la licence CMF de la structure',
+    cmf_structure_licence: "Licence CMF de la structure",
+    your_cmf_licence: "Votre licence CMF",
+    cmf_licence_details_url: "Consulter les avantages de la licence CMF",
+    generate: "Générer",
     parameters: 'Préférences',
     place: 'Lieux',
     education: 'Enseignements',

+ 8 - 2
layouts/default.vue

@@ -23,7 +23,13 @@
 </template>
 
 <script lang="ts">
-import { computed, ComputedRef, defineComponent, reactive, useContext } from '@nuxtjs/composition-api'
+import {
+  computed,
+  ComputedRef,
+  defineComponent,
+  reactive,
+  useContext
+} from '@nuxtjs/composition-api'
 import { $useMenu } from '@/composables/layout/menu'
 
 export default defineComponent({
@@ -31,7 +37,7 @@ export default defineComponent({
 
   middleware: ['auth'],
   setup () {
-    const { store } = useContext()
+    const { store, $config } = useContext()
     const menu = $useMenu.setupContext().useLateralMenuConstruct()
 
     const properties = reactive({

+ 3 - 0
models/Core/File.ts

@@ -23,4 +23,7 @@ export class File extends Model {
 
   @Num(null, { nullable: true })
   ownerId!: number
+
+  @Str('')
+  status!: 'PENDING' | 'READY' | 'ERROR'
 }

+ 17 - 0
models/Export/LicenceCmfOrganizationER.ts

@@ -0,0 +1,17 @@
+import {Bool, Model, Num, Str, Uid} from "@vuex-orm/core";
+
+export class LicenceCmfOrganizationER extends Model {
+  static entity = 'licence_cmf_'
+
+  @Uid()
+  id!: number | string | null
+
+  @Str(null, {nullable: true})
+  format!: string | null
+
+  @Num(0, {nullable: false})
+  requesterId!: number | string | null
+
+  @Bool(false, {nullable: false})
+  async!: boolean
+}

+ 2 - 14
models/Organization/OrganizationNetwork.ts

@@ -1,20 +1,8 @@
 import {Str, Model, Uid} from '@vuex-orm/core'
 
-export class OrganizationLicence extends Model {
-  static entity = 'organization_licences'
+export class OrganizationNetwork extends Model {
+  static entity = 'organization_networks'
 
   @Uid()
   id!: number | string | null
-
-  @Str('', { nullable: false })
-  licensee!: string
-
-  @Str(null, { nullable: true })
-  licenceNumber!: string|null
-
-  @Str(null, { nullable: true })
-  categorie!: string|null
-
-  @Str(null, { nullable: true })
-  validityDate!: string|null
 }

+ 40 - 0
models/_import.ts

@@ -0,0 +1,40 @@
+import {MyProfile} from "~/models/Access/MyProfile";
+import {PersonalizedList} from "~/models/Access/PersonalizedList";
+import {AddressPostal} from "~/models/Core/AddressPostal";
+import {BankAccount} from "~/models/Core/BankAccount";
+import {ContactPoint} from "~/models/Core/ContactPoint";
+import {Country} from "~/models/Core/Country";
+import {File} from "~/models/Core/File";
+import {Notification} from "~/models/Core/Notification";
+import {NotificationMessage} from "~/models/Core/NotificationMessage";
+import {NotificationUsers} from "~/models/Core/NotificationUsers";
+import {LicenceCmfOrganizationER} from "~/models/Export/LicenceCmfOrganizationER";
+import {Network} from "~/models/Network/Network";
+import {NetworkOrganization} from "~/models/Network/NetworkOrganization";
+import {Organization} from "~/models/Organization/Organization";
+import {OrganizationAddressPostal} from "~/models/Organization/OrganizationAddressPostal";
+import {OrganizationArticle} from "~/models/Organization/OrganizationArticle";
+import {OrganizationNetwork} from "~/models/Organization/OrganizationNetwork";
+import {TypeOfPractice} from "~/models/Organization/TypeOfPractice";
+
+
+export const models: Array<any> = [
+  MyProfile,
+  PersonalizedList,
+  AddressPostal,
+  BankAccount,
+  ContactPoint,
+  Country,
+  File,
+  Notification,
+  NotificationMessage,
+  NotificationUsers,
+  LicenceCmfOrganizationER,
+  Network,
+  NetworkOrganization,
+  Organization,
+  OrganizationAddressPostal,
+  OrganizationArticle,
+  OrganizationNetwork,
+  TypeOfPractice
+]

+ 3 - 1
package.json

@@ -36,6 +36,7 @@
     "@vuex-orm/core": "1.0.0-draft.16",
     "cookieparser": "^0.1",
     "core-js": "^3.17",
+    "event-source-polyfill": "^1.0.26",
     "js-yaml": "^4.0",
     "libphonenumber-js": "^1.9.39",
     "lodash": "^4.17",
@@ -59,6 +60,7 @@
     "@nuxtjs/eslint-config-typescript": "^6.0",
     "@nuxtjs/eslint-module": "^3.0",
     "@nuxtjs/moment": "^1.6",
+    "@types/event-source-polyfill": "^1.0.0",
     "@types/jest": "^27.0",
     "@types/vue-the-mask": "^0.11.1",
     "@vue/test-utils": "^1.1",
@@ -70,7 +72,7 @@
     "eslint": "^7.32",
     "eslint-plugin-nuxt": "^2.0",
     "jest": "^27.1",
-    "jsdoc": "^3.6",
+    "jsdoc": "^3.6.10",
     "postcss-import": "^13.0",
     "postcss-loader": "^4.1",
     "postcss-url": "^10.1",

+ 24 - 0
pages/cmf_licence/access.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="d-flex flex-column align-center">
+    <h2 class="ma-4">{{ $t('your_cmf_licence')}}</h2>
+    <a
+      href="https://www.cmf-musique.org/services/tarifs-preferentiels/"
+      target="_blank"
+    >
+      {{ $t('cmf_licence_details_url')}}
+    </a>
+
+    <v-btn class="ma-12">{{ $t('generate') }}</v-btn>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from "@nuxtjs/composition-api";
+
+export default defineComponent({
+  name: 'AccessCmfLicence',
+  setup() {
+    return {}
+  }
+})
+</script>

+ 103 - 0
pages/cmf_licence/organization.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="d-flex flex-column align-center">
+    <h2 class="ma-4">{{ $t('cmf_structure_licence')}}</h2>
+    <a
+      href="https://www.cmf-musique.org/services/tarifs-preferentiels/"
+      target="_blank"
+    >
+      {{ $t('cmf_licence_details_url')}}
+    </a>
+
+    <v-form
+      ref="form"
+      lazy-validation
+    >
+      <div class="ma-12">
+        <v-btn
+          v-if="!pending && file === null"
+          @click="submit"
+        >
+          {{ $t('generate') }}
+        </v-btn>
+
+        <v-btn
+          v-else
+          color="primary"
+          :loading="pending"
+          :disabled="pending"
+          :href="file ? file.url : ''">
+          {{ $t('download') }}
+        </v-btn>
+      </div>
+    </v-form>
+  </div>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent, Ref, ref, useContext} from "@nuxtjs/composition-api";
+import {QUERY_TYPE} from "~/types/enums";
+import DataPersister from "~/services/data/dataPersister";
+import {DataPersisterArgs} from "~/types/interfaces";
+import { Context } from "@nuxt/types";
+import {Repository as VuexRepository} from "@vuex-orm/core/dist/src/repository/Repository";
+import {Model, Query} from "@vuex-orm/core";
+import {repositoryHelper} from "~/services/store/repository";
+import {File} from "~/models/Core/File";
+import {queryHelper} from "~/services/store/query";
+
+export default defineComponent({
+  name: 'OrganizationCmfLicence',
+  setup() {
+    const context = useContext()
+
+    let fileId: Ref<number | null> = ref(null)
+
+    let file: ComputedRef<File | null> = computed(() => {
+      return fileId.value !== null ? queryHelper.getItem(query, fileId.value) as File : null
+    })
+
+    let pending: ComputedRef<boolean> = computed(() => {
+      return file.value !== null && file.value.status === 'PENDING'
+    })
+
+    const async = () => { return context.store.state.sse.connected }
+
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(File)
+    const query: Query = repository.query()
+
+    const submit = async () => {
+      const dataPersister = new DataPersister()
+      dataPersister.initCtx(context as unknown as Context)
+
+      const response = await dataPersister.invoke(
+        {
+          url: '/api/export/cmf-licence/organization',
+          type: QUERY_TYPE.DEFAULT,
+          data: { format: 'pdf', async: async() },
+          withCredentials: true
+        } as DataPersisterArgs
+      )
+
+      if (!async()) {
+        console.error('SSE unavailable - File downloaded synchronously')
+      }
+
+      fileId.value = response.data.id
+      repositoryHelper.persist(File, response.data)
+    }
+
+    return {
+      submit,
+      pending,
+      file
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(File)
+  }
+})
+
+function useSse(): { sseConnected: any; } {
+    throw new Error("Function not implemented.");
+}
+</script>

+ 23 - 0
plugins/sse.ts

@@ -0,0 +1,23 @@
+import {Plugin} from "@nuxt/types";
+import SseSource from "~/services/sse/sseSource";
+
+/**
+ * Setup SSE EventSource, allowing the app to listen for Mercure updates
+ * /!\ This has to be executed client side
+ *
+ * @param ctx
+ */
+const ssePlugin: Plugin = ({ $config, store }) => {
+  const sseSource = new SseSource(
+    $config.baseUrl_mercure,
+    "access/" + store.state.profile.access.id,
+    () => { store.commit('sse/setConnected', true) },
+    (eventData) => { store.commit('sse/addEvent', eventData) },
+    () => { store.commit('sse/setConnected', false) },
+  )
+
+  sseSource.subscribe()
+  window.addEventListener('beforeunload', () => { sseSource.unsubscribe() })
+}
+
+export default ssePlugin

+ 11 - 5
services/connection/connection.ts

@@ -1,5 +1,5 @@
 import {NuxtAxiosInstance} from '@nuxtjs/axios'
-import {AxiosRequestConfig} from 'axios'
+import {AxiosRequestConfig, AxiosResponse} from 'axios'
 import {AnyJson, DataPersisterArgs, DataProviderArgs, UrlArgs} from '~/types/interfaces'
 import {HTTP_METHOD, QUERY_TYPE} from '~/types/enums'
 import TypesTesting from "~/services/utils/typesTesting";
@@ -45,8 +45,14 @@ class Connection {
         if (!args.data) {
           throw new Error('*args* has no data')
         }
-        return method === HTTP_METHOD.PUT ? Connection.put(url, args.id, args.data, args.showProgress, args.params) :
-                                            Connection.post(url, args.data, args.showProgress, args.params)
+
+        switch (method) {
+          case HTTP_METHOD.PUT:
+            return Connection.put(url, args.id, args.data, args.showProgress, args.params)
+          case HTTP_METHOD.POST:
+            return Connection.post(url, args.data, args.showProgress, args.params)
+        }
+        break;
 
       case HTTP_METHOD.DELETE:
         return Connection.deleteItem(url, args.id, args.showProgress, args.params)
@@ -102,7 +108,7 @@ class Connection {
    * @param {AnyJson} params
    * @return {Promise<any>}
    */
-  public static post (url: string, data: AnyJson, progress: boolean = true, params: AnyJson = {}): Promise<any> {
+  public static post(url: string, data: AnyJson, progress: boolean = true, params: AnyJson = {}): Promise<any> {
     const config: AxiosRequestConfig = {
       url: `${url}`,
       method: HTTP_METHOD.POST,
@@ -156,7 +162,7 @@ class Connection {
    * @param {AxiosRequestConfig} config
    * @return {Promise<any>}
    */
-  public static async request (config: AxiosRequestConfig): Promise<any> {
+  public static async request (config: AxiosRequestConfig): Promise<AxiosResponse> {
     return await Connection.connector.$request(config)
   }
 }

+ 1 - 1
services/data/baseDataManager.ts

@@ -63,7 +63,7 @@ abstract class BaseDataManager extends Hookable implements DataManager {
    * @param {HTTP_METHOD} method
    * @param {UrlArgs} args
    */
-  protected static request (url: string, method: HTTP_METHOD, args: UrlArgs): Promise<any> {
+  public static request (url: string, method: HTTP_METHOD, args: UrlArgs): Promise<any> {
     return Connection.invoke(method, url, args)
   }
 }

+ 1 - 1
services/data/processor/modelProcessor.ts

@@ -15,7 +15,7 @@ class ModelProcessor extends BaseProcessor implements Processor {
 
   /**
    * Exécute la requête et retourne la réponse désérialisée
-   * @param data
+   * @param payload
    */
   async process (payload: ApiResponse): Promise<any> {
     if (typeof this.arguments.model === 'undefined') {

+ 1 - 2
services/serializer/denormalizer/hydra.ts

@@ -27,11 +27,10 @@ class Hydra extends BaseDenormalizer {
   }
 
   private static parseItem (hydraData: AnyJson): ApiResponse {
-    const itemResponse: ApiResponse = {
+    return {
       data: hydraData,
       metadata: Hydra.definedMetadataForItem(hydraData)
     }
-    return itemResponse
   }
     /**
    * Méthode de parsing appelé si on est dans un GET

+ 74 - 0
services/sse/sseSource.ts

@@ -0,0 +1,74 @@
+import { EventSourcePolyfill } from "event-source-polyfill";
+
+class SseSource {
+  private readonly url: URL
+  private readonly onOpen: (() => void)
+  private readonly onMessage: ((eventData: Array<any>) => void)
+  private readonly onClose: (() => void)
+  private readonly withCredentials: boolean
+  protected eventSource: EventSource | null = null
+
+  constructor(
+    mercureHubBaseUrl: string,
+    topic: string,
+    onOpen: (() => void),
+    onMessage: ((eventData: Array<any>) => void),
+    onClose: (() => void),
+    withCredentials: boolean = true
+  ) {
+    this.url = new URL(mercureHubBaseUrl)
+    this.url.searchParams.append('topic', topic)
+    this.onOpen = onOpen
+    this.onMessage = onMessage
+    this.onClose = onClose
+    this.withCredentials = withCredentials
+  }
+
+  protected createEventSource(url: string, withCredentials: boolean): EventSourcePolyfill {
+    return new EventSourcePolyfill(
+      url,
+      {
+        withCredentials: withCredentials,
+        heartbeatTimeout: 45 * 1000 // in ms, timeout can not be disabled yet, so I set it very large instead
+      }
+    );
+  }
+
+  public isConnected () {
+    return this.eventSource !== null && this.eventSource.readyState === EventSourcePolyfill.OPEN
+  }
+
+  public subscribe () {
+    if (this.isConnected()) {
+      throw new Error('SSE - Already subscribed to this event source')
+    }
+    if (process.server) {
+      throw new Error('SSE - Cannot subscribe on server side')
+    }
+
+    this.eventSource = this.createEventSource(this.url.toString(), this.withCredentials)
+
+    this.eventSource.onerror = (event) => {
+      console.error('SSE - An error happened')
+    }
+    this.eventSource.onopen = () => {
+      console.log('SSE - Listening for events...')
+      this.onOpen()
+    }
+    this.eventSource.onmessage = event => {
+      const data = JSON.parse(event.data)
+      this.onMessage(data)
+    }
+  }
+
+  public unsubscribe () {
+    if (this.eventSource === null || this.eventSource.readyState === EventSource.CLOSED) {
+      return
+    }
+    this.eventSource.close()
+    this.onClose()
+    console.log('SSE - Subscription closed')
+  }
+}
+
+export default SseSource

+ 21 - 0
services/utils/modelsUtils.ts

@@ -1,3 +1,5 @@
+import {models} from "~/models/_import";
+
 export default class ModelsUtils {
   /**
    * Extrait l'ID de l'URI passée en paramètre
@@ -14,4 +16,23 @@ export default class ModelsUtils {
 
     return parseInt(id)
   }
+  /**
+   * Extrait l'ID de l'URI passée en paramètre
+   * @param iri
+   */
+  static getModelFromIri (iri: string): any {
+    const matches = iri.match(/^\/api\/(\w+)\/.*/)
+    if (!matches || !matches[1]) {
+      throw new Error('cannot parse the IRI')
+    }
+    const entityName = matches[1]
+
+    let model = models.find(candidate => { return candidate.entity === entityName })
+    if (!model) {
+      throw new Error('no model found')
+    }
+    return model
+  }
+
+
 }

+ 30 - 0
store/sse.ts

@@ -0,0 +1,30 @@
+import {MercureEntityUpdate, sseState} from "~/types/interfaces";
+import {repositoryHelper} from "~/services/store/repository";
+import ModelsUtils from "~/services/utils/modelsUtils";
+
+export const state = () => ({
+  connected: false,
+})
+
+export const mutations = {
+  setConnected(state: sseState, connected: boolean) {
+    state.connected = connected
+  },
+  addEvent(state: sseState, event: MercureEntityUpdate) {
+
+    const model = ModelsUtils.getModelFromIri(event.iri)
+
+    switch (event.operation) {
+      case "update":
+      case "create":
+        repositoryHelper.persist(model, JSON.parse(event.data))
+        break
+
+      case "delete":
+        break
+
+      default:
+        throw new Error('SSE: unknown operation type')
+    }
+  }
+}

+ 57 - 0
tests/unit/services/sse/sseSource.spec.ts

@@ -0,0 +1,57 @@
+import SseSource from "~/services/sse/sseSource";
+import {EventSourcePolyfill} from "event-source-polyfill";
+
+class TestableSseSource extends SseSource {
+  public eventSource: EventSourcePolyfill | null = null
+  public createEventSource(url: string, withCredentials: boolean): EventSourcePolyfill {
+    return super.createEventSource(url, withCredentials)
+  }
+}
+
+describe('SseSource', () => {
+  describe('createEventSource', () => {
+    it('returnValidEventSourcePolyfill', () => {
+      const sseSource = new TestableSseSource(
+        'http://mercure',
+        '',
+        () => {},
+        (eventData) => {},
+        () => {}
+      )
+
+      const eventSource = sseSource.createEventSource('http://mercure', true)
+
+      expect(eventSource.url).toEqual('http://mercure')
+      expect(eventSource.withCredentials).toEqual(true)
+    })
+  })
+
+  describe('isConnected', () => {
+    it('is true when readyState is open', () =>
+    {
+      const eventSource = new EventSourcePolyfill('http://mercure')
+      Object.defineProperty(eventSource, 'readyState', {value: EventSourcePolyfill.OPEN})
+
+      const sseSource = new TestableSseSource(
+        'http://mercure',
+        '',
+        () => {},
+        (eventData) => {},
+        () => {}
+      )
+
+      sseSource.eventSource = eventSource
+      expect(sseSource.isConnected()).toBeTruthy()
+    })
+    it('is false else', () => {
+      const sseSource = new TestableSseSource(
+        'http://mercure',
+        '',
+        () => {},
+        (eventData) => {},
+        () => {}
+      )
+      expect(sseSource.isConnected()).toBeFalsy()
+    })
+  })
+})

+ 10 - 0
types/interfaces.d.ts

@@ -288,3 +288,13 @@ interface HydraMetadata {
   type?: METADATA_TYPE
 }
 
+interface MercureEntityUpdate {
+  iri: string,
+  operation: 'create' | 'delete' | 'update',
+  data: any
+}
+
+interface sseState {
+  connected: boolean,
+  events: Array<MercureEntityUpdate>,
+}