Repository: interledger/web-monetization-extension (ts)

Issue:
Configure popup store
### Context

- #45

### Todos

- Information stored in the extension storage should be sent to the popup
- Setup a context provider that registers of all the information that was sent from the extension
- The amount that needs to be sent per second (derived from the rate of pay value that is received from the extension storage)

Info: related to task #58 

Pull request description:
feat: Popup Context
## Context

Closes #59 

## Changes proposed in this pull request

- add provider wrapper with context data
- add unit tests

Patch:
diff --git a/package.json b/package.json
index 7cb1f3b..7324df8 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
     "test:ci": "pnpm test -- --reporters=default --reporters=github-actions"
   },
   "dependencies": {
+    "awilix": "^10.0.1",
     "axios": "^1.5.1",
     "class-variance-authority": "^0.7.0",
     "clean-webpack-plugin": "^4.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 043d41b..0fabcb8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,6 +5,9 @@ settings:
   excludeLinksFromLockfile: false
 
 dependencies:
+  awilix:
+    specifier: ^10.0.1
+    version: 10.0.1
   axios:
     specifier: ^1.5.1
     version: 1.6.2
@@ -2337,6 +2340,14 @@ packages:
     engines: {node: '>= 0.4'}
     dev: true
 
+  /awilix@10.0.1:
+    resolution: {integrity: sha512-+ylXK26rx4BVfzAB9AXQE+k/nomshpW8ISR+vZN2QecMiCoaT8km4DaIO0nazYSCX6nZCHYOFnoeCa5cE13NEA==}
+    engines: {node: '>=14.0.0'}
+    dependencies:
+      camel-case: 4.1.2
+      fast-glob: 3.3.2
+    dev: false
+
   /aws-sign2@0.7.0:
     resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
     dev: false
diff --git a/src/background/Background.ts b/src/background/Background.ts
new file mode 100644
index 0000000..f13911f
--- /dev/null
+++ b/src/background/Background.ts
@@ -0,0 +1,58 @@
+import { runtime, tabs } from 'webextension-polyfill'
+
+import { PaymentFlowService } from '@/background/grantFlow'
+
+import getSendingPaymentPointerHandler from '../messageHandlers/getSendingPaymentPointerHandler'
+import isMonetizationReadyHandler from '../messageHandlers/isMonetizationReadyHandler'
+import setIncomingPointerHandler from '../messageHandlers/setIncomingPointerHandler'
+import { tabChangeHandler, tabUpdateHandler } from './tabHandlers'
+
+class Background {
+  private messageHandlers: any = [
+    isMonetizationReadyHandler,
+    setIncomingPointerHandler,
+    getSendingPaymentPointerHandler,
+  ]
+  private subscriptions: any = []
+  // TO DO: remove these from background into storage or state & use injection
+  grantFlow: PaymentFlowService | null = null
+  spentAmount: number = 0
+  paymentStarted = false
+
+  constructor() {}
+
+  subscribeToMessages() {
+    this.subscriptions = this.messageHandlers.map((handler: any) => {
+      const listener: any = async (message: EXTMessage) => {
+        if (handler.type === message.type) {
+          try {
+            await handler.callback(message.data, this)
+          } catch (error) {
+            console.log('[===== Error in MessageListener =====]', error)
+            return error
+          }
+        }
+      }
+
+      runtime.onMessage.addListener(listener)
+
+      return () => {
+        runtime.onMessage.removeListener(listener)
+      }
+    })
+  }
+
+  subscribeToTabChanges() {
+    //Add Update listener for tab
+    tabs.onUpdated.addListener(tabUpdateHandler)
+
+    //Add tab change listener
+    tabs.onActivated.addListener(tabChangeHandler)
+  }
+
+  unsubscribeFromMessages() {
+    this.subscriptions.forEach((sub: any) => sub())
+  }
+}
+
+export default Background
diff --git a/src/background/BackgroundContainer.ts b/src/background/BackgroundContainer.ts
new file mode 100644
index 0000000..08d31d4
--- /dev/null
+++ b/src/background/BackgroundContainer.ts
@@ -0,0 +1,12 @@
+import { asClass, createContainer } from 'awilix'
+
+import Background from './Background'
+
+const BackgroundContainer = createContainer()
+
+BackgroundContainer.register({
+  Background: asClass(Background).singleton(),
+  // TODO - add injectable services
+})
+
+export default BackgroundContainer
diff --git a/src/background/index.ts b/src/background/index.ts
index 7c65b53..f3bb864 100755
--- a/src/background/index.ts
+++ b/src/background/index.ts
@@ -1,264 +1,14 @@
-import { action, Runtime, runtime, Tabs, tabs } from 'webextension-polyfill'
+import BackgroundContainer from './BackgroundContainer'
 
-import { PaymentFlowService } from '@/background/grantFlow'
+const initialize = () => {
+  console.log('Start initialization')
 
-const iconActive34 = runtime.getURL('assets/icons/icon-active-34.png')
-const iconActive128 = runtime.getURL('assets/icons/icon-active-128.png')
-const iconInactive34 = runtime.getURL('assets/icons/icon-inactive-34.png')
-const iconInactive128 = runtime.getURL('assets/icons/icon-inactive-128.png')
+  const background = BackgroundContainer.resolve('Background')
 
-// const SENDING_PAYMENT_POINTER_URL = 'https://ilp.rafiki.money/wmuser' // cel din extensie al userului
-// const RECEIVING_PAYMENT_POINTER_URL = 'https://ilp.rafiki.money/web-page' // cel din dom
+  background.subscribeToMessages()
+  background.subscribeToTabChanges()
 
-/**
- * Define background script functions
- * @type {class}
- */
-class Background {
-  _port: number
-  grantFlow: PaymentFlowService | null = null
-  spentAmount: number = 0
-  paymentStarted = false
-
-  constructor() {
-    this.init()
-  }
-
-  /**
-   * Document Ready
-   *
-   * @returns {void}
-   */
-  init = async () => {
-    console.log('[===== Loaded Background Scripts =====]')
-
-    //When extension installed
-    runtime.onInstalled.addListener(this.onInstalled)
-
-    //Add message listener in Browser.
-    runtime.onMessage.addListener(this.onMessage)
-
-    //Add Update listener for tab
-    tabs.onUpdated.addListener(this.onUpdatedTab)
-
-    //Add New tab create listener
-    tabs.onCreated.addListener(this.onCreatedTab)
-
-    //Add tab change listener
-    tabs.onActivated.addListener(this.handleTabChange)
-  }
-
-  //TODO: Listeners
-
-  /**
-   * Extension Installed
-   */
-  onInstalled = () => {
-    console.log('[===== Installed Extension!] =====')
-  }
-
-  /**
-   * Message Handler Function
-   *
-   * @param message
-   * @param sender
-   * @returns
-   */
-  onMessage = async (message: EXTMessage, sender: Runtime.MessageSender) => {
-    try {
-      console.log('[===== Received message =====]', message, sender)
-      switch (message.type) {
-        case 'IS_MONETIZATION_READY': {
-          if (message?.data) {
-            this.updateIcon(message.data.monetization)
-          }
-          break
-        }
-
-        case 'SET_INCOMING_POINTER': {
-          const {
-            incomingPayment: receivingPaymentPointerUrl,
-            paymentPointer: sendingPaymentPointerUrl,
-            amount,
-          } = message.data
-
-          if (this.grantFlow?.sendingPaymentPointerUrl === sendingPaymentPointerUrl) {
-            if (!this.paymentStarted) {
-              this.paymentStarted = true
-              const currentTabId = await this.grantFlow?.getCurrentActiveTabId()
-              await tabs.sendMessage(currentTabId ?? 0, { type: 'START_PAYMENTS' })
-            }
-          } else {
-            this.grantFlow = new PaymentFlowService(
-              sendingPaymentPointerUrl,
-              receivingPaymentPointerUrl,
-              amount,
-            )
-
-            this.grantFlow.initPaymentFlow()
-          }
-
-          break
-        }
-
-        case 'GET_SENDING_PAYMENT_POINTER': {
-          if (this.grantFlow) {
-            const { sendingPaymentPointerUrl, amount } = this.grantFlow
-            return {
-              type: 'SUCCESS',
-              data: { sendingPaymentPointerUrl, amount, started: this.paymentStarted },
-            }
-          }
-
-          return {
-            type: 'ERROR',
-            data: { sendingPaymentPointerUrl: '' },
-          }
-        }
-
-        case 'RUN_PAYMENT': {
-          if (this.grantFlow) {
-            this.grantFlow.sendPayment()
-            this.spentAmount = Number(
-              parseFloat(String(this.spentAmount + 1000000 / 10 ** 9)).toFixed(3),
-            )
-            this.sendSpendAmount()
-            this.paymentStarted = true
-          }
-          break
-        }
-
-        case 'PAUSE_PAYMENTS': {
-          this.paymentStarted = false
-          break
-        }
-      }
-
-      return true // result to reply
-    } catch (error) {
-      console.log('[===== Error in MessageListener =====]', error)
-      return error
-    }
-  }
-
-  /**
-   * Message from Long Live Connection
-   *
-   * @param msg
-   */
-  onMessageFromExtension = (msg: EXTMessage) => {
-    console.log('[===== Message from Long Live Connection =====]', msg)
-  }
-
-  /**
-   *
-   * @param tab
-   */
-  onCreatedTab = (tab: Tabs.Tab) => {
-    console.log('[===== New Tab Created =====]', tab)
-  }
-
-  sendSpendAmount() {
-    runtime.sendMessage({
-      type: 'SPENT_AMOUNT',
-      data: { spentAmount: this.spentAmount },
-    })
-  }
-
-  /**
-   * When changes tabs
-   *
-   * @param {*} tabId
-   * @param {*} changeInfo
-   * @param {*} tab
-   */
-  onUpdatedTab = async (tabId: number, changeInfo: Tabs.OnUpdatedChangeInfoType, tab: Tabs.Tab) => {
-    if (tab.status === 'complete' && tab.url?.match(/^http/)) {
-      const response = await this.sendMessage(tab, { type: 'IS_MONETIZATION_READY' })
-      if (response.data) {
-        await this.updateIcon(response.data.monetization)
-      }
-    }
-  }
-
-  /**
-   * Get url from tabId
-   *
-   */
-  getURLFromTab = async (tabId: number) => {
-    try {
-      const tab = await tabs.get(tabId)
-      return tab.url || ''
-    } catch (error) {
-      console.log(`[===== Could not get Tab Info$(tabId) in getURLFromTab =====]`, error)
-      throw ''
-    }
-  }
-
-  /**
-   * Open new tab by url
-   *
-   */
-  openNewTab = async (url: string) => {
-    try {
-      const tab = await tabs.create({ url })
-      return tab
-    } catch (error) {
-      console.log(`[===== Error in openNewTab =====]`, error)
-      return null
-    }
-  }
-
-  /**
-   * Close specific tab
-   *
-   * @param {number} tab
-   */
-  closeTab = async (tab: Tabs.Tab) => {
-    try {
-      await tabs.remove(tab.id ?? 0)
-    } catch (error) {
-      console.log(`[===== Error in closeTab =====]`, error)
-    }
-  }
-
-  /**
-   * send message
-   */
-  sendMessage = async (tab: Tabs.Tab, msg: EXTMessage) => {
-    try {
-      const res = await tabs.sendMessage(tab.id ?? 0, msg)
-      return res
-    } catch (error) {
-      console.log(`[===== Error in sendMessage =====]`, error)
-      return null
-    }
-  }
-
-  updateIcon = async (active: boolean) => {
-    const iconData = {
-      '34': active ? iconActive34 : iconInactive34,
-      '128': active ? iconActive128 : iconInactive128,
-    }
-
-    if (action) {
-      await action.setIcon({ path: iconData })
-    } else if (chrome.browserAction) {
-      chrome.browserAction.setIcon({ path: iconData })
-    }
-  }
-
-  handleTabChange = async (activeInfo: chrome.tabs.TabActiveInfo) => {
-    const tabId = activeInfo.tabId
-
-    const tab = await tabs.get(tabId)
-    if (tab && tab.url?.includes('https') && tab.status === 'complete') {
-      const response = await this.sendMessage(tab, { type: 'IS_MONETIZATION_READY' })
-      if (response?.data) {
-        this.updateIcon(response.data.monetization)
-      }
-    }
-  }
+  console.log('End initialization')
 }
 
-export const background = new Background()
+initialize()
diff --git a/src/background/tabHandlers.ts b/src/background/tabHandlers.ts
new file mode 100644
index 0000000..2e84a15
--- /dev/null
+++ b/src/background/tabHandlers.ts
@@ -0,0 +1,38 @@
+import { Tabs, tabs } from 'webextension-polyfill'
+
+import { sendMessageToTab } from '@/utils/sendMessages'
+
+import { updateIcon } from './utils'
+
+export const tabChangeHandler = async (activeInfo: chrome.tabs.TabActiveInfo) => {
+  const tabId = activeInfo.tabId
+  const tab = await tabs.get(tabId)
+
+  if (tab && tab.url?.includes('https') && tab.status === 'complete') {
+    try {
+      const response = await sendMessageToTab(tab, { type: 'IS_MONETIZATION_READY' })
+      if (response?.data) {
+        updateIcon(response.data.monetization)
+      }
+    } catch (error) {
+      console.log(`[===== Error in tabChangeHandler =====]`, error)
+    }
+  }
+}
+
+export const tabUpdateHandler = async (
+  tabId: number,
+  changeInfo: Tabs.OnUpdatedChangeInfoType,
+  tab: Tabs.Tab,
+) => {
+  if (tab.status === 'complete' && tab.url?.match(/^http/)) {
+    try {
+      const response = await sendMessageToTab(tab, { type: 'IS_MONETIZATION_READY' })
+      if (response?.data) {
+        updateIcon(response.data.monetization)
+      }
+    } catch (error) {
+      console.log(`[===== Error in tabUpdateHandler =====]`, error)
+    }
+  }
+}
diff --git a/src/background/utils.ts b/src/background/utils.ts
new file mode 100644
index 0000000..92173ac
--- /dev/null
+++ b/src/background/utils.ts
@@ -0,0 +1,19 @@
+import { action, runtime } from 'webextension-polyfill'
+
+const iconActive34 = runtime.getURL('assets/icons/icon-active-34.png')
+const iconActive128 = runtime.getURL('assets/icons/icon-active-128.png')
+const iconInactive34 = runtime.getURL('assets/icons/icon-inactive-34.png')
+const iconInactive128 = runtime.getURL('assets/icons/icon-inactive-128.png')
+
+export const updateIcon = async (active: boolean) => {
+  const iconData = {
+    '34': active ? iconActive34 : iconInactive34,
+    '128': active ? iconActive128 : iconInactive128,
+  }
+
+  if (action) {
+    await action.setIcon({ path: iconData })
+  } else if (chrome.browserAction) {
+    chrome.browserAction.setIcon({ path: iconData })
+  }
+}
diff --git a/src/messageHandlers/getSendingPaymentPointerHandler.ts b/src/messageHandlers/getSendingPaymentPointerHandler.ts
new file mode 100644
index 0000000..b6f8864
--- /dev/null
+++ b/src/messageHandlers/getSendingPaymentPointerHandler.ts
@@ -0,0 +1,18 @@
+import Background from '@/background/Background'
+
+const getSendingPaymentPointerCallback = async (data: undefined, background: Background) => {
+  if (background.grantFlow) {
+    const { sendingPaymentPointerUrl, amount } = background.grantFlow
+    return {
+      type: 'SUCCESS',
+      data: { sendingPaymentPointerUrl, amount, started: background.paymentStarted },
+    }
+  }
+
+  return {
+    type: 'ERROR',
+    data: { sendingPaymentPointerUrl: '' },
+  }
+}
+
+export default { callback: getSendingPaymentPointerCallback, type: 'GET_SENDING_PAYMENT_POINTER' }
diff --git a/src/messageHandlers/isMonetizationReadyHandler.ts b/src/messageHandlers/isMonetizationReadyHandler.ts
new file mode 100644
index 0000000..9bb2cfc
--- /dev/null
+++ b/src/messageHandlers/isMonetizationReadyHandler.ts
@@ -0,0 +1,13 @@
+import { updateIcon } from '@/background/utils'
+
+export type IsMonetizationReadyData = {
+  monetization: boolean
+}
+
+const isMometizationReadyCallback = async (data: IsMonetizationReadyData) => {
+  await updateIcon(data.monetization)
+
+  return true
+}
+
+export default { callback: isMometizationReadyCallback, type: 'IS_MONETIZATION_READY' }
diff --git a/src/messageHandlers/pausePaymentHandler.ts b/src/messageHandlers/pausePaymentHandler.ts
new file mode 100644
index 0000000..80688c9
--- /dev/null
+++ b/src/messageHandlers/pausePaymentHandler.ts
@@ -0,0 +1,9 @@
+import Background from '@/background/Background'
+
+const pausePaymentCallback = async (data: undefined, background: Background) => {
+  background.paymentStarted = false
+
+  return true
+}
+
+export default { callback: pausePaymentCallback, type: 'PAUSE_PAYMENT' }
diff --git a/src/messageHandlers/runPaymentHandler.ts b/src/messageHandlers/runPaymentHandler.ts
new file mode 100644
index 0000000..19e3110
--- /dev/null
+++ b/src/messageHandlers/runPaymentHandler.ts
@@ -0,0 +1,21 @@
+import { runtime } from 'webextension-polyfill'
+
+import Background from '@/background/Background'
+
+const runPaymentCallback = async (data: undefined, background: Background) => {
+  if (background.grantFlow) {
+    background.grantFlow.sendPayment()
+    background.spentAmount = Number(
+      parseFloat(String(background.spentAmount + 1000000 / 10 ** 9)).toFixed(3),
+    )
+    runtime.sendMessage({
+      type: 'SPENT_AMOUNT',
+      data: { spentAmount: background.spentAmount },
+    })
+    background.paymentStarted = true
+  }
+
+  return true
+}
+
+export default { callback: runPaymentCallback, type: 'RUN_PAYMENT' }
diff --git a/src/messageHandlers/setIncomingPointerHandler.ts b/src/messageHandlers/setIncomingPointerHandler.ts
new file mode 100644
index 0000000..ca0b23b
--- /dev/null
+++ b/src/messageHandlers/setIncomingPointerHandler.ts
@@ -0,0 +1,38 @@
+import { tabs } from 'webextension-polyfill'
+
+import Background from '@/background/Background'
+import { PaymentFlowService } from '@/background/grantFlow'
+
+export type SetIncomingPointerData = {
+  incomingPayment: string
+  paymentPointer: string
+  amount: string
+}
+
+const setIncomingPointerCallback = async (data: SetIncomingPointerData, background: Background) => {
+  const {
+    incomingPayment: receivingPaymentPointerUrl,
+    paymentPointer: sendingPaymentPointerUrl,
+    amount,
+  } = data
+
+  if (background.grantFlow?.sendingPaymentPointerUrl === sendingPaymentPointerUrl) {
+    if (!background.paymentStarted) {
+      background.paymentStarted = true
+      const currentTabId = await background.grantFlow?.getCurrentActiveTabId()
+      await tabs.sendMessage(currentTabId ?? 0, { type: 'START_PAYMENTS' })
+    }
+  } else {
+    background.grantFlow = new PaymentFlowService(
+      sendingPaymentPointerUrl,
+      receivingPaymentPointerUrl,
+      amount,
+    )
+
+    background.grantFlow.initPaymentFlow()
+  }
+
+  return true
+}
+
+export default { callback: setIncomingPointerCallback, type: 'SET_INCOMING_POINTER' }
diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx
index 89d305c..82d733f 100644
--- a/src/popup/Popup.tsx
+++ b/src/popup/Popup.tsx
@@ -3,9 +3,14 @@ import './Popup.scss'
 import React from 'react'
 
 import { RouterProvider } from '@/components/router-provider'
+import { PopupProvider } from '@/providers/popup-context'
 
 const Popup = () => {
-  return <RouterProvider />
+  return (
+    <PopupProvider>
+      <RouterProvider />
+    </PopupProvider>
+  )
 }
 
 export default Popup
diff --git a/src/providers/index.ts b/src/providers/index.ts
new file mode 100644
index 0000000..35b996f
--- /dev/null
+++ b/src/providers/index.ts
@@ -0,0 +1,3 @@
+import { PopupProvider } from './popup-context'
+
+export { PopupProvider }
diff --git a/src/providers/popup-context.tsx b/src/providers/popup-context.tsx
new file mode 100644
index 0000000..392f2c7
--- /dev/null
+++ b/src/providers/popup-context.tsx
@@ -0,0 +1,33 @@
+import React, { createContext, useState } from 'react'
+
+import { PopupContextValue, TPopupContext } from './providers.interface'
+
+export const defaultData = {
+  amount: 0,
+  amountType: {
+    recurring: true,
+  },
+  rateOfPay: 0.36,
+  wmEnabled: true,
+  accessTokenQuote: '',
+  accessTokenOutgoing: '',
+  refreshToken: '',
+  manageUrl: '',
+}
+
+interface IProps {
+  children: React.ReactNode
+}
+
+export const PopupContext = createContext<PopupContextValue>({
+  data: defaultData,
+  setData: () => {},
+})
+
+export const PopupProvider: React.FC<IProps> = ({ children }) => {
+  const [data, setData] = useState<TPopupContext>(defaultData)
+
+  return <PopupContext.Provider value={{ data, setData }}>{children}</PopupContext.Provider>
+}
+
+export default PopupProvider
diff --git a/src/providers/providers.interface.ts b/src/providers/providers.interface.ts
new file mode 100644
index 0000000..9be002f
--- /dev/null
+++ b/src/providers/providers.interface.ts
@@ -0,0 +1,19 @@
+import React from 'react'
+
+export interface PopupContextValue {
+  data: TPopupContext
+  setData: React.Dispatch<React.SetStateAction<TPopupContext>>
+}
+
+export type TPopupContext = {
+  amount: number
+  amountType: {
+    recurring: boolean
+  }
+  rateOfPay: number
+  wmEnabled: boolean
+  accessTokenQuote: string
+  accessTokenOutgoing: string
+  refreshToken: string
+  manageUrl: string
+}
diff --git a/tsconfig.json b/tsconfig.json
index a568303..428a387 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,6 +21,7 @@
       "@/content/*": ["./content/*"],
       "@/assets/*": ["./assets/*"],
       "@/components/*": ["./components/*"],
+      "@/providers/*": ["./providers/*"],
       "@/types/*": ["./types/*"],
       "@/hooks/*": ["./hooks/*"]
     }
diff --git a/webpack.config.utils.ts b/webpack.config.utils.ts
index 176e457..3361087 100644
--- a/webpack.config.utils.ts
+++ b/webpack.config.utils.ts
@@ -217,6 +217,7 @@ export const getResolves = () => {
       '@/content': path.resolve(__dirname, './src/content/'),
       '@/assets': path.resolve(__dirname, './src/assets/'),
       '@/components': path.resolve(__dirname, './src/components/'),
+      '@/providers': path.resolve(__dirname, './src/providers/'),
       '@/types': path.resolve(__dirname, './src/types/'),
       '@/hooks': path.resolve(__dirname, './src/hooks/'),
     },


Test patch:
diff --git a/src/providers/__tests__/popup-context.test.tsx b/src/providers/__tests__/popup-context.test.tsx
new file mode 100644
index 0000000..ed841a6
--- /dev/null
+++ b/src/providers/__tests__/popup-context.test.tsx
@@ -0,0 +1,90 @@
+import { act, render, screen } from '@testing-library/react'
+import React, { useContext } from 'react'
+
+import { defaultData, PopupContext, PopupProvider } from '../popup-context'
+
+const TestComponent = () => {
+  const { data, setData } = useContext(PopupContext)
+
+  return (
+    <div>
+      <div data-testid="amount">{data.amount}</div>
+      <div data-testid="rateOfPay">{data.rateOfPay}</div>
+      <button onClick={() => setData({ ...data, amount: 100 })}>Update Amount</button>
+    </div>
+  )
+}
+
+const MockChildComponent = () => {
+  const { data } = useContext(PopupContext)
+  return <div data-testid="mock-child">{data.amount}</div>
+}
+
+describe('PopupProvider', () => {
+  it('provides the initial context values', () => {
+    render(
+      <PopupProvider>
+        <TestComponent />
+      </PopupProvider>,
+    )
+
+    expect(screen.getByTestId('amount').textContent).toBe('0')
+    expect(screen.getByTestId('rateOfPay').textContent).toBe('0.36')
+  })
+
+  it('updates context values when setData is called', () => {
+    render(
+      <PopupProvider>
+        <TestComponent />
+      </PopupProvider>,
+    )
+
+    act(() => {
+      screen.getByText('Update Amount').click()
+    })
+
+    expect(screen.getByTestId('amount').textContent).toBe('100')
+  })
+
+  it('displays different initial values correctly', () => {
+    const customInitialData = {
+      ...defaultData,
+      amount: 50,
+      rateOfPay: 0.45,
+    }
+
+    render(
+      <PopupContext.Provider value={{ data: customInitialData, setData: () => {} }}>
+        <TestComponent />
+      </PopupContext.Provider>,
+    )
+
+    expect(screen.getByTestId('amount').textContent).toBe('50')
+    expect(screen.getByTestId('rateOfPay').textContent).toBe('0.45')
+  })
+
+  it('handles multiple updates to context values', () => {
+    render(
+      <PopupProvider>
+        <TestComponent />
+      </PopupProvider>,
+    )
+
+    act(() => {
+      screen.getByText('Update Amount').click()
+      screen.getByText('Update Amount').click()
+    })
+
+    expect(screen.getByTestId('amount').textContent).toBe('100')
+  })
+
+  it('renders child component with context data', () => {
+    render(
+      <PopupProvider>
+        <MockChildComponent />
+      </PopupProvider>,
+    )
+
+    expect(screen.getByTestId('mock-child').textContent).toBe('0')
+  })
+})