From c539d90133e118e56953fe14a6dd6fbf6245e923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Mon, 18 Mar 2024 12:30:22 +0100 Subject: [PATCH 01/33] chore(web): the playwright e2e tests poc --- apps/web/.env.playwirght.test | 2 + apps/web/.eslintrc.js | 2 +- apps/web/.github/workflows/playwright.yml | 27 ++ apps/web/.gitignore | 6 +- apps/web/package.json | 5 + apps/web/playwright.config.ts | 87 ++++ apps/web/tests-examples/demo-todo-app.spec.ts | 416 ++++++++++++++++++ apps/web/tests/example.spec.ts | 18 + apps/web/tests/main-functionality.spec.ts | 176 ++++++++ apps/web/tests/utils.ts/browser.ts | 22 + apps/web/tests/utils.ts/plugins.ts | 51 +++ apps/web/tests/utils.ts/workflow-editor.ts | 61 +++ libs/shared-web/src/config.ts | 21 +- pnpm-lock.yaml | 57 ++- 14 files changed, 929 insertions(+), 22 deletions(-) create mode 100644 apps/web/.env.playwirght.test create mode 100644 apps/web/.github/workflows/playwright.yml create mode 100644 apps/web/playwright.config.ts create mode 100644 apps/web/tests-examples/demo-todo-app.spec.ts create mode 100644 apps/web/tests/example.spec.ts create mode 100644 apps/web/tests/main-functionality.spec.ts create mode 100644 apps/web/tests/utils.ts/browser.ts create mode 100644 apps/web/tests/utils.ts/plugins.ts create mode 100644 apps/web/tests/utils.ts/workflow-editor.ts diff --git a/apps/web/.env.playwirght.test b/apps/web/.env.playwirght.test new file mode 100644 index 00000000000..d5d0a22a1f1 --- /dev/null +++ b/apps/web/.env.playwirght.test @@ -0,0 +1,2 @@ +NODE_ENV=test +MONGODB_URL=mongodb://127.0.0.1:27017/novu-test diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index a882dc23378..4417ceb2f7a 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -41,7 +41,7 @@ module.exports = { env: { 'cypress/globals': true, }, - ignorePatterns: ['craco.config.js', 'cypress/*', '**/styled-system/**/*'], + ignorePatterns: ['craco.config.js', 'cypress/*', '**/styled-system/**/*', 'tests/*'], extends: ['plugin:cypress/recommended', '../../.eslintrc.js'], plugins: ['cypress', 'react-hooks'], parserOptions: { diff --git a/apps/web/.github/workflows/playwright.yml b/apps/web/.github/workflows/playwright.yml new file mode 100644 index 00000000000..9662b54e97f --- /dev/null +++ b/apps/web/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 44f85e781ec..3267e0dcdb1 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -29,4 +29,8 @@ cypress/screenshots ## Panda styled-system -styled-system-studio \ No newline at end of file +styled-system-studio +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/apps/web/package.json b/apps/web/package.json index aa59763cb3c..ff264e7fcca 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,6 +19,10 @@ "cypress:install": "cypress install", "cypress:open": "cross-env NODE_ENV=test cypress open", "cypress:run:components": "cross-env NODE_OPTIONS=--max_old_space_size=4096 NODE_ENV=test cypress run --component", + "playwright:test": "playwright test", + "playwright:test-ui": "playwright test --ui", + "playwright:codegen": "playwright codegen", + "playwright:show-report": "npx playwright show-report", "start:api": "cd ../../ && pnpm start:api:test", "storybook": "storybook dev -p 6006 -s public", "build-storybook": "storybook build -s public", @@ -137,6 +141,7 @@ "@novu/dal": "^0.24.0", "@novu/testing": "^0.24.0", "@pandacss/dev": "^0.34.0", + "@playwright/test": "^1.42.1", "@storybook/addon-actions": "^7.4.2", "@storybook/addon-essentials": "^7.4.2", "@storybook/addon-links": "^7.4.2", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts new file mode 100644 index 00000000000..0ee7fe32eca --- /dev/null +++ b/apps/web/playwright.config.ts @@ -0,0 +1,87 @@ +import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '.env.playwirght.test') }); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://127.0.0.1:4200', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + timeout: 60_000, + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + /* + * { + * name: 'Mobile Chrome', + * use: { ...devices['Pixel 5'] }, + * }, + * { + * name: 'Mobile Safari', + * use: { ...devices['iPhone 12'] }, + * }, + */ + + /* Test against branded browsers. */ + /* + * { + * name: 'Microsoft Edge', + * use: { ...devices['Desktop Edge'], channel: 'msedge' }, + * }, + * { + * name: 'Google Chrome', + * use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + * }, + */ + ], + + /* Run your local dev server before starting the tests */ + /* + * webServer: { + * command: 'npm run start', + * url: 'http://127.0.0.1:3000', + * reuseExistingServer: !process.env.CI, + * }, + */ +}); diff --git a/apps/web/tests-examples/demo-todo-app.spec.ts b/apps/web/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 00000000000..28a8669dd86 --- /dev/null +++ b/apps/web/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,416 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = ['buy some cheese', 'feed the cat', 'book a doctors appointment']; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0]]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count'); + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect( + todoItem.locator('label', { + hasText: TODO_ITEMS[1], + }) + ).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count'); + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction((t) => { + return JSON.parse(localStorage['react-todos']) + .map((todo: any) => todo.title) + .includes(t); + }, title); +} diff --git a/apps/web/tests/example.spec.ts b/apps/web/tests/example.spec.ts new file mode 100644 index 00000000000..54a906a4e84 --- /dev/null +++ b/apps/web/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/apps/web/tests/main-functionality.spec.ts b/apps/web/tests/main-functionality.spec.ts new file mode 100644 index 00000000000..bf23af54417 --- /dev/null +++ b/apps/web/tests/main-functionality.spec.ts @@ -0,0 +1,176 @@ +import { test, expect } from '@playwright/test'; +import os from 'node:os'; + +import { getByTestId, initializeSession } from './utils.ts/browser'; +import { + addAndEditChannel, + editChannel, + fillBasicNotificationDetails, + goBack, + updateWorkflowButtonClick, +} from './utils.ts/workflow-editor'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context); +}); + +test('should not reset data when switching channel types', async ({ page }) => { + await page.goto('/workflows/create'); + + await fillBasicNotificationDetails(page); + await goBack(page); + await addAndEditChannel(page, 'inApp'); + + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('{{firstName}} someone assigned you to {{taskName}}'); + + await goBack(page); + await addAndEditChannel(page, 'email'); + + const subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.fill('this is email subject'); + + const preheaderEl = getByTestId(page, 'emailPreheader'); + await preheaderEl.fill('this is email preheader'); + + const editableText = getByTestId(page, 'editable-text-content'); + await editableText.clear(); + await editableText.pressSequentially('This text is written from a test {{firstName}}'); + + await goBack(page); + + await editChannel(page, 'inApp'); + await goBack(page); + + await editChannel(page, 'email'); + await expect(getByTestId(page, 'emailSubject')).toHaveValue('this is email subject'); + await expect(getByTestId(page, 'emailPreheader')).toHaveValue('this is email preheader'); + await expect(getByTestId(page, 'editable-text-content')).toContainText('This text is written from a test'); +}); + +test('should update to empty data when switching from editor to customHtml', async ({ page }) => { + await page.goto('/workflows/create'); + + await fillBasicNotificationDetails(page, { title: 'Test Notification' }); + await goBack(page); + await addAndEditChannel(page, 'email'); + + const editableText = getByTestId(page, 'editable-text-content'); + await editableText.clear(); + await editableText.pressSequentially('This text is written from a test {{firstName}}'); + + let subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.fill('this is email subject'); + + await updateWorkflowButtonClick(page); + + await page + .locator('[data-test-id="editor-type-selector"] .mantine-Tabs-tabsList') + .getByText(/Custom Code/) + .first() + .click(); + + subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.clear(); + await subjectEl.fill('new email subject'); + + await updateWorkflowButtonClick(page, { noWaitAfter: true }); + + const templatesLinkPage = getByTestId(page, 'side-nav-templates-link'); + await templatesLinkPage.click(); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(/Test Notification/).click(); + + await editChannel(page, 'email'); + + await expect( + page + .locator('[data-test-id="editor-type-selector"] .mantine-Tabs-tabsList') + .locator('[data-active="true"]') + .getByText(/Custom Code/) + .first() + ).toBeVisible(); +}); + +test('should save avatar enabled and content for in app', async ({ page }) => { + await page.goto('/workflows/create'); + + await fillBasicNotificationDetails(page, { title: 'Test save avatar' }); + await goBack(page); + await addAndEditChannel(page, 'inApp'); + + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('new content for notification'); + + const enableAddAvatar = getByTestId(page, 'enable-add-avatar'); + await enableAddAvatar.click(); + const chooseAvatar = getByTestId(page, 'choose-avatar-btn'); + await chooseAvatar.click(); + const avatarIconInfo = getByTestId(page, 'avatar-icon-info'); + await avatarIconInfo.click(); + + await updateWorkflowButtonClick(page); + + expect(getByTestId(page, 'enabled-avatar')).toBeChecked(); + expect(getByTestId(page, 'avatar-icon-info')).toBeVisible(); +}); + +test('should edit in-app notification', async ({ page }) => { + const template = session.templates[0]; + await page.goto(`/workflows/edit/${template._id}`); + + const settingsPage = getByTestId(page, 'settings-page'); + await settingsPage.click(); + + let nameInput = getByTestId(page, 'name-input'); + await expect(nameInput.first()).toHaveValue(template.name); + + await goBack(page); + await editChannel(page, 'inApp'); + + let editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('Test content for {{firstName}}'); + + await goBack(page); + + nameInput = getByTestId(page, 'name-input'); + await nameInput.clear(); + await nameInput.fill('This is the new notification title'); + + await editChannel(page, 'inApp'); + const feedButton = getByTestId(page, 'feed-button-1'); + await feedButton.click(); + + const monacoEditor = page.locator('.monaco-editor').nth(0); + await monacoEditor.click(); + const isMac = os.platform() === 'darwin'; + const modifier = isMac ? 'Meta' : 'Control'; + await monacoEditor.press(`${modifier}+KeyX`); + await page.keyboard.type('new content for notification'); + + await goBack(page); + + await updateWorkflowButtonClick(page); + + await page.goto(`/workflows`); + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await expect(await notificationsTemplate.getByText(/This is the new notification title/)).toBeVisible(); + + await page.goto(`/workflows/edit/${template._id}`); + await editChannel(page, 'inApp'); + + await expect(getByTestId(page, 'feed-button-0')).toBeVisible(); + await expect(getByTestId(page, 'feed-button-1-checked')).toBeVisible(); + const createFeedInput = getByTestId(page, 'create-feed-input'); + await createFeedInput.fill('test4'); + + const addFeedButton = getByTestId(page, 'add-feed-button'); + await addFeedButton.click(); + await expect(getByTestId(page, 'feed-button-2-checked')).toBeVisible(); +}); diff --git a/apps/web/tests/utils.ts/browser.ts b/apps/web/tests/utils.ts/browser.ts new file mode 100644 index 00000000000..2b8725e89b0 --- /dev/null +++ b/apps/web/tests/utils.ts/browser.ts @@ -0,0 +1,22 @@ +import { BrowserContext, Locator, Page } from '@playwright/test'; +import { getSession } from './plugins'; + +export async function initializeSession(context: BrowserContext) { + const session = await getSession(); + + await context.addInitScript((session) => { + (window as any).isPlaywright = true; + localStorage.setItem('auth_token', session.token); + }, session); + + return session; +} + +export function getByTestId(page: Page | Locator, selector: string, options?: Parameters[1]) { + return page.locator(`[data-test-id="${selector}"]`, options); +} + +export async function dragAndDrop(page: Page, dragSelector: string, dropSelector: string) { + const dndEl = await getByTestId(page, dragSelector); + await dndEl.dragTo(await getByTestId(page, dropSelector), { force: true }); +} diff --git a/apps/web/tests/utils.ts/plugins.ts b/apps/web/tests/utils.ts/plugins.ts new file mode 100644 index 00000000000..ccca680fb95 --- /dev/null +++ b/apps/web/tests/utils.ts/plugins.ts @@ -0,0 +1,51 @@ +import { DalService, NotificationTemplateEntity } from '@novu/dal'; +import { UserSession, NotificationTemplateService } from '@novu/testing'; + +export async function getSession( + settings: { + noEnvironment?: boolean; + partialTemplate?: Partial; + noTemplates?: boolean; + showOnBoardingTour?: boolean; + } = {} +) { + const dal = new DalService(); + await dal.connect(process.env.MONGODB_URL ?? ''); + + const session = new UserSession('http://127.0.0.1:1336'); + await session.initialize({ + noEnvironment: settings?.noEnvironment, + showOnBoardingTour: settings?.showOnBoardingTour, + }); + + const notificationTemplateService = new NotificationTemplateService( + session.user._id, + session.organization._id, + session.environment._id as string + ); + + let templates; + if (!settings?.noTemplates) { + const templatePartial = settings?.partialTemplate || {}; + + templates = await Promise.all([ + notificationTemplateService.createTemplate({ ...(templatePartial as any) }), + notificationTemplateService.createTemplate({ + active: false, + draft: true, + }), + notificationTemplateService.createTemplate(), + notificationTemplateService.createTemplate(), + notificationTemplateService.createTemplate(), + notificationTemplateService.createTemplate(), + ]); + } + + return { + token: session.token.split(' ')[1], + user: session.user, + organization: session.organization, + environment: session.environment, + templates, + }; +} diff --git a/apps/web/tests/utils.ts/workflow-editor.ts b/apps/web/tests/utils.ts/workflow-editor.ts new file mode 100644 index 00000000000..90ab651843e --- /dev/null +++ b/apps/web/tests/utils.ts/workflow-editor.ts @@ -0,0 +1,61 @@ +import { Page } from '@playwright/test'; + +import { dragAndDrop, getByTestId } from './browser'; + +export type Channel = 'inApp' | 'email' | 'sms' | 'chat' | 'push' | 'digest' | 'delay'; + +export async function fillBasicNotificationDetails( + page: Page, + { title, description }: { title?: string; description?: string } = {} +) { + const settings = await getByTestId(page, 'settings-page'); + await settings.click(); + + const titleEl = await getByTestId(page, 'title'); + await titleEl.first().clear(); + await titleEl.first().fill(title ?? 'Test Notification Title'); + + const descriptionEl = await getByTestId(page, 'description'); + await descriptionEl.fill(description ?? 'This is a test description for a test title'); +} + +export async function goBack(page: Page) { + const closeButton = await getByTestId(page, 'sidebar-close'); + await closeButton.click(); +} + +export async function editChannel(page: Page, channel: Channel) { + const stepNode = await getByTestId(page, `node-${channel}Selector`); + await stepNode.last().click(); + + if (['inApp', 'email', 'sms', 'chat', 'push'].includes(channel)) { + const sidebarComponent = await getByTestId(page, 'step-editor-sidebar'); + const editButton = await getByTestId(sidebarComponent, 'edit-action'); + await editButton.click(); + } +} + +export async function addAndEditChannel(page: Page, channel: Channel) { + await dragAndDrop(page, `dnd-${channel}Selector`, 'addNodeButton'); + await editChannel(page, channel); +} + +export async function updateWorkflowButtonClick(page: Page, { noWaitAfter = false }: { noWaitAfter?: boolean } = {}) { + if (noWaitAfter) { + const updateWorkflowButton = getByTestId(page, 'notification-template-submit-btn'); + await updateWorkflowButton.click(); + + return; + } + + const updateTemplateRequest = page.waitForResponse((response) => { + return response.url().match(/\/v1\/notification-templates\/.*/) && response.request().method() === 'PUT'; + }); + const getTemplateRequest = page.waitForResponse((response) => { + return response.url().match(/\/v1\/notification-templates\/.*/) && response.request().method() === 'GET'; + }); + let updateWorkflowButton = getByTestId(page, 'notification-template-submit-btn'); + await updateWorkflowButton.click(); + await updateTemplateRequest; + await getTemplateRequest; +} diff --git a/libs/shared-web/src/config.ts b/libs/shared-web/src/config.ts index a8b77f3875d..28bb19d4270 100644 --- a/libs/shared-web/src/config.ts +++ b/libs/shared-web/src/config.ts @@ -12,15 +12,17 @@ declare global { } const isCypress = (isBrowser() && (window as any).Cypress) || (isBrowser() && (window as any).parent.Cypress); +const isPlaywright = isBrowser() && (window as any).isPlaywright; export const API_ROOT = - window._env_.REACT_APP_API_URL || isCypress + window._env_.REACT_APP_API_URL || isCypress || isPlaywright ? window._env_.REACT_APP_API_URL || process.env.REACT_APP_API_URL || 'http://localhost:1336' : window._env_.REACT_APP_API_URL || process.env.REACT_APP_API_URL || 'http://localhost:3000'; -export const WS_URL = isCypress - ? window._env_.REACT_APP_WS_URL || process.env.REACT_APP_WS_URL || 'http://localhost:1340' - : window._env_.REACT_APP_WS_URL || process.env.REACT_APP_WS_URL || 'http://localhost:3002'; +export const WS_URL = + isCypress || isPlaywright + ? window._env_.REACT_APP_WS_URL || process.env.REACT_APP_WS_URL || 'http://localhost:1340' + : window._env_.REACT_APP_WS_URL || process.env.REACT_APP_WS_URL || 'http://localhost:3002'; export const SENTRY_DSN = window._env_.REACT_APP_SENTRY_DSN || process.env.REACT_APP_SENTRY_DSN; @@ -29,7 +31,7 @@ export const ENV = window._env_.REACT_APP_ENVIRONMENT || process.env.REACT_APP_E const blueprintApiUrlByEnv = ENV === 'production' || ENV === 'prod' ? 'https://api.novu.co' : 'https://dev.api.novu.co'; export const BLUEPRINTS_API_URL = - window._env_.REACT_APP_BLUEPRINTS_API_URL || isCypress + window._env_.REACT_APP_BLUEPRINTS_API_URL || isCypress || isPlaywright ? window._env_.REACT_APP_BLUEPRINTS_API_URL || process.env.REACT_APP_BLUEPRINTS_API_URL || 'http://localhost:1336' : blueprintApiUrlByEnv; @@ -49,9 +51,10 @@ export const INTERCOM_APP_ID = window._env_.REACT_APP_INTERCOM_APP_ID || process export const CONTEXT_PATH = getContextPath(NovuComponentEnum.WEB); -export const WEBHOOK_URL = isCypress - ? window._env_.REACT_APP_WEBHOOK_URL || process.env.REACT_APP_WEBHOOK_URL || 'http://localhost:1341' - : window._env_.REACT_APP_WEBHOOK_URL || process.env.REACT_APP_WEBHOOK_URL || 'http://localhost:3003'; +export const WEBHOOK_URL = + isCypress || isPlaywright + ? window._env_.REACT_APP_WEBHOOK_URL || process.env.REACT_APP_WEBHOOK_URL || 'http://localhost:1341' + : window._env_.REACT_APP_WEBHOOK_URL || process.env.REACT_APP_WEBHOOK_URL || 'http://localhost:3003'; export const MAIL_SERVER_DOMAIN = window._env_.REACT_APP_MAIL_SERVER_DOMAIN || process.env.REACT_APP_MAIL_SERVER_DOMAIN || 'dev.inbound-mail.novu.co'; @@ -60,7 +63,7 @@ export const LAUNCH_DARKLY_CLIENT_SIDE_ID = window._env_.REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID || process.env.REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID; export const FEATURE_FLAGS = Object.values(FeatureFlagsKeysEnum).reduce((acc, key) => { - const defaultValue = isCypress ? true : false; + const defaultValue = isCypress || isPlaywright ? true : false; acc[key] = window._env_[key] || process.env[key] || defaultValue; return acc; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d235eca175..780ebcb5345 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -944,6 +944,9 @@ importers: '@pandacss/dev': specifier: ^0.34.0 version: 0.34.0(jsdom@24.0.0)(typescript@4.9.5) + '@playwright/test': + specifier: ^1.42.1 + version: 1.42.1 '@storybook/addon-actions': specifier: ^7.4.2 version: 7.4.2(@types/react-dom@17.0.19)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) @@ -22436,6 +22439,14 @@ packages: tslib: 2.6.2 dev: true + /@playwright/test@1.42.1: + resolution: {integrity: sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.42.1 + dev: true + /@plunk/node@2.0.0: resolution: {integrity: sha512-53lgots3fWGAo1QdS18BdEpJl7A29O1F9rYVn/7DfJ07SpJ1ZlzUeeWGVrWGL7PRRZb4a9Tw7Tt8Wnw0Xorhjg==} dependencies: @@ -32724,6 +32735,7 @@ packages: acorn: ^8 dependencies: acorn: 8.11.3 + dev: false /acorn-jsx@5.3.2(acorn@7.4.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -37425,7 +37437,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 - dev: true /debug@3.2.7(supports-color@8.1.1): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -38711,7 +38722,7 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.13.0 resolve: 1.22.2 transitivePeerDependencies: @@ -38726,7 +38737,7 @@ packages: webpack: '>=1.11.0' dependencies: array.prototype.find: 2.2.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) enhanced-resolve: 0.9.1 eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-webpack@0.13.7)(eslint@8.38.0) find-root: 1.1.0 @@ -38764,7 +38775,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.58.0(eslint@8.38.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-webpack: 0.13.7(eslint-plugin-import@2.28.1)(webpack@5.78.0) @@ -38794,7 +38805,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.58.0(eslint@8.51.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-webpack: 0.13.7(eslint-plugin-import@2.28.1)(webpack@5.78.0) @@ -38875,7 +38886,7 @@ packages: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 @@ -38910,7 +38921,7 @@ packages: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 @@ -40682,6 +40693,14 @@ packages: dev: true optional: true + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -43631,7 +43650,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@16.11.7)(typescript@4.9.5) transitivePeerDependencies: - bufferutil - canvas @@ -47820,7 +47839,7 @@ packages: hasBin: true requiresBuild: true dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) iconv-lite: 0.6.3 sax: 1.2.4 transitivePeerDependencies: @@ -50118,6 +50137,22 @@ packages: find-up: 3.0.0 dev: true + /playwright-core@1.42.1: + resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==} + engines: {node: '>=16'} + hasBin: true + dev: true + + /playwright@1.42.1: + resolution: {integrity: sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.42.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /please-upgrade-node@3.2.0: resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} dependencies: @@ -50196,7 +50231,7 @@ packages: engines: {node: '>= 0.12.0'} dependencies: async: 2.6.4 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) mkdirp: 0.5.6 transitivePeerDependencies: - supports-color @@ -54073,7 +54108,7 @@ packages: jest-worker: 26.6.2 rollup: 2.79.1 serialize-javascript: 4.0.0 - terser: 5.16.9 + terser: 5.22.0 dev: true /rollup-plugin-terser@7.0.2(rollup@3.20.2): From 6001cd23788ec2705e35b0b6cd8a271bb5c72275 Mon Sep 17 00:00:00 2001 From: Biswajeet Das Date: Thu, 21 Mar 2024 12:14:24 +0530 Subject: [PATCH 02/33] fix: tooltip border --- .../activities/components/ActivityGraphGlobalStyles.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/web/src/pages/activities/components/ActivityGraphGlobalStyles.tsx b/apps/web/src/pages/activities/components/ActivityGraphGlobalStyles.tsx index e8268c8ec07..01f53c738f7 100644 --- a/apps/web/src/pages/activities/components/ActivityGraphGlobalStyles.tsx +++ b/apps/web/src/pages/activities/components/ActivityGraphGlobalStyles.tsx @@ -38,11 +38,9 @@ function chartStyles(isTriggerSent: boolean, isDark: boolean) { .tooltip-title { display: flex; - justify-content: center; height: 17px; margin-bottom: 4px; - border-width: 22px; color: ${colors.B60}; } @@ -51,10 +49,7 @@ function chartStyles(isTriggerSent: boolean, isDark: boolean) { display: flex; justify-content: center; font-weight: 700; - height: 17px; - border-width: 22px; - color: #ff512f; background: -webkit-linear-gradient(90deg, #dd2476 0%, #ff512f 100%); -webkit-background-clip: text; From 977a5bc9c200f9e4939baee94d5e02a439242bbc Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:42:05 +0100 Subject: [PATCH 03/33] fix(worker): Skip template rendering for Echo Workflow steps (#5365) --- .../send-message/send-message-chat.usecase.ts | 14 ++++---- .../send-message-in-app.usecase.ts | 36 ++++++++++--------- .../send-message/send-message-push.usecase.ts | 26 +++++++------- .../send-message/send-message-sms.usecase.ts | 14 ++++---- 4 files changed, 49 insertions(+), 41 deletions(-) diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts index 4a710fca18d..286885c7166 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts @@ -89,12 +89,14 @@ export class SendMessageChat extends SendMessageBase { let content = ''; try { - content = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: step.template.content as string, - data: this.getCompilePayload(command.compileContext), - }) - ); + if (!command.chimeraData) { + content = await this.compileTemplate.execute( + CompileTemplateCommand.create({ + template: step.template.content as string, + data: this.getCompilePayload(command.compileContext), + }) + ); + } } catch (e) { await this.sendErrorHandlebars(command.job, e.message); diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-in-app.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-in-app.usecase.ts index 557b708b073..49648cc3b7a 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-in-app.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-in-app.usecase.ts @@ -107,25 +107,27 @@ export class SendMessageInApp extends SendMessageBase { } try { - const compiled = await this.compileInAppTemplate.execute( - CompileInAppTemplateCommand.create({ - organizationId: command.organizationId, - environmentId: command.environmentId, - payload: this.getCompilePayload(command.compileContext), - content: step.template.content as string, - cta: step.template.cta, - userId: command.userId, - }), - this.initiateTranslations.bind(this) - ); - content = compiled.content; + if (!command.chimeraData) { + const compiled = await this.compileInAppTemplate.execute( + CompileInAppTemplateCommand.create({ + organizationId: command.organizationId, + environmentId: command.environmentId, + payload: this.getCompilePayload(command.compileContext), + content: step.template.content as string, + cta: step.template.cta, + userId: command.userId, + }), + this.initiateTranslations.bind(this) + ); + content = compiled.content; - if (step.template.cta?.data?.url) { - step.template.cta.data.url = compiled.url; - } + if (step.template.cta?.data?.url) { + step.template.cta.data.url = compiled.url; + } - if (step.template.cta?.action?.buttons) { - step.template.cta.action.buttons = compiled.ctaButtons; + if (step.template.cta?.action?.buttons) { + step.template.cta.action.buttons = compiled.ctaButtons; + } } } catch (e) { await this.sendErrorHandlebars(command.job, e.message); diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts index 3d50da8706d..070901ad582 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts @@ -91,19 +91,21 @@ export class SendMessagePush extends SendMessageBase { let title = ''; try { - content = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: step.template?.content as string, - data, - }) - ); + if (!command.chimeraData) { + content = await this.compileTemplate.execute( + CompileTemplateCommand.create({ + template: step.template?.content as string, + data, + }) + ); - title = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: step.template?.title as string, - data, - }) - ); + title = await this.compileTemplate.execute( + CompileTemplateCommand.create({ + template: step.template?.title as string, + data, + }) + ); + } } catch (e) { await this.sendErrorHandlebars(command.job, e.message); diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts index d26436ddc93..55cbabdd12f 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts @@ -89,12 +89,14 @@ export class SendMessageSms extends SendMessageBase { let content: string | null = ''; try { - content = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: step.template.content as string, - data: this.getCompilePayload(command.compileContext), - }) - ); + if (!command.chimeraData) { + content = await this.compileTemplate.execute( + CompileTemplateCommand.create({ + template: step.template.content as string, + data: this.getCompilePayload(command.compileContext), + }) + ); + } } catch (e) { await this.sendErrorHandlebars(command.job, e.message); From 7c553aa244610206162e5cf4407f890eb3a6a647 Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:44:19 +0100 Subject: [PATCH 04/33] chore(infra): Remove CI steps for redundant general worker (#5366) --- .github/workflows/dev-deploy-worker.yml | 14 +--------- .github/workflows/prod-deploy-worker.yml | 33 ++---------------------- 2 files changed, 3 insertions(+), 44 deletions(-) diff --git a/.github/workflows/dev-deploy-worker.yml b/.github/workflows/dev-deploy-worker.yml index cfbfd53d0c9..03629d0ca8a 100644 --- a/.github/workflows/dev-deploy-worker.yml +++ b/.github/workflows/dev-deploy-worker.yml @@ -64,20 +64,8 @@ jobs: docker_name: ${{ matrix.name }} bullmq_secret: ${{ secrets.BULL_MQ_PRO_NPM_TOKEN }} - # Temporary for the migration phase - deploy_general_worker: - needs: build_dev_worker - uses: ./.github/workflows/reusable-app-service-deploy.yml - secrets: inherit - with: - environment: Development - service_name: worker - terraform_workspace: novu-dev - # This is a workaround to an issue with matrix outputs - docker_image: ghcr.io/novuhq/novu/worker-ee:${{ github.sha }} - deploy_dev_workers: - needs: deploy_general_worker + needs: build_dev_worker uses: ./.github/workflows/reusable-workers-service-deploy.yml secrets: inherit with: diff --git a/.github/workflows/prod-deploy-worker.yml b/.github/workflows/prod-deploy-worker.yml index 8b0daade313..242c47e88fe 100644 --- a/.github/workflows/prod-deploy-worker.yml +++ b/.github/workflows/prod-deploy-worker.yml @@ -95,20 +95,8 @@ jobs: docker push ghcr.io/$REGISTRY_OWNER/$DOCKER_NAME:$IMAGE_TAG echo "IMAGE=ghcr.io/$REGISTRY_OWNER/$DOCKER_NAME:$IMAGE_TAG" >> $GITHUB_OUTPUT - # Temporary for the migration phase - deploy_general_worker_eu: - needs: build_prod_image - uses: ./.github/workflows/reusable-app-service-deploy.yml - secrets: inherit - with: - environment: Production - service_name: worker - terraform_workspace: novu-prod-eu - # This is a workaround to an issue with matrix outputs - docker_image: ghcr.io/novuhq/novu/worker-ee:${{ github.sha }} - deploy_prod_workers_eu: - needs: deploy_general_worker_eu + needs: build_prod_image uses: ./.github/workflows/reusable-workers-service-deploy.yml secrets: inherit with: @@ -117,25 +105,8 @@ jobs: # This is a workaround to an issue with matrix outputs docker_image: ghcr.io/novuhq/novu/worker-ee:${{ github.sha }} - - # Temporary for the migration phase - deploy_general_worker_us: - needs: - - deploy_prod_workers_eu - - build_prod_image - uses: ./.github/workflows/reusable-app-service-deploy.yml - secrets: inherit - with: - environment: Production - service_name: worker - terraform_workspace: novu-prod - # This is a workaround to an issue with matrix outputs - docker_image: ghcr.io/novuhq/novu/worker-ee:${{ github.sha }} - deploy_prod_workers_us: - needs: - - deploy_general_worker_us - - build_prod_image + needs: build_prod_image uses: ./.github/workflows/reusable-workers-service-deploy.yml secrets: inherit with: From b50e8187fcfb3513fb569b26ec50b5c31fcd4a6d Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:38:34 +0100 Subject: [PATCH 05/33] fix(worker): Move missing SMS content check inside echo conditional (#5369) --- .../usecases/send-message/send-message-sms.usecase.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts index 55cbabdd12f..8098303c613 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts @@ -96,6 +96,10 @@ export class SendMessageSms extends SendMessageBase { data: this.getCompilePayload(command.compileContext), }) ); + + if (!content) { + throw new PlatformException(`Unexpected error: SMS content is missing`); + } } } catch (e) { await this.sendErrorHandlebars(command.job, e.message); @@ -103,10 +107,6 @@ export class SendMessageSms extends SendMessageBase { return; } - if (!content) { - throw new PlatformException(`Unexpected error: SMS content is missing`); - } - const phone = command.payload.phone || subscriber.phone; if (!integration) { From d06ca8c8f021a447c9a1b993f88e221c38857475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Mon, 8 Apr 2024 00:24:24 +0200 Subject: [PATCH 06/33] chore(web): a few more tests for the main functionality --- apps/web/tests/example.spec.ts | 18 ------- apps/web/tests/main-functionality.spec.ts | 65 ++++++++++++++++++++++- 2 files changed, 63 insertions(+), 20 deletions(-) delete mode 100644 apps/web/tests/example.spec.ts diff --git a/apps/web/tests/example.spec.ts b/apps/web/tests/example.spec.ts deleted file mode 100644 index 54a906a4e84..00000000000 --- a/apps/web/tests/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/apps/web/tests/main-functionality.spec.ts b/apps/web/tests/main-functionality.spec.ts index bf23af54417..c0c4fbc7d6e 100644 --- a/apps/web/tests/main-functionality.spec.ts +++ b/apps/web/tests/main-functionality.spec.ts @@ -12,6 +12,9 @@ import { let session; +const isMac = os.platform() === 'darwin'; +const modifier = isMac ? 'Meta' : 'Control'; + test.beforeEach(async ({ context }) => { session = await initializeSession(context); }); @@ -149,8 +152,6 @@ test('should edit in-app notification', async ({ page }) => { const monacoEditor = page.locator('.monaco-editor').nth(0); await monacoEditor.click(); - const isMac = os.platform() === 'darwin'; - const modifier = isMac ? 'Meta' : 'Control'; await monacoEditor.press(`${modifier}+KeyX`); await page.keyboard.type('new content for notification'); @@ -174,3 +175,63 @@ test('should edit in-app notification', async ({ page }) => { await addFeedButton.click(); await expect(getByTestId(page, 'feed-button-2-checked')).toBeVisible(); }); + +test('should unset feedId for in app step', async ({ page }) => { + const template = session.templates[0]; + + await page.goto(`/workflows/edit/${template._id}`); + await editChannel(page, 'inApp'); + + let feedsCheckbox = getByTestId(page, 'use-feeds-checkbox'); + await expect(feedsCheckbox).toBeChecked(); + await feedsCheckbox.click(); + + await updateWorkflowButtonClick(page); + + await page.goto(`/workflows`); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await expect(notificationsTemplate.getByText(template.name, { exact: false })).toBeVisible(); + + await page.goto(`/workflows/edit/${template._id}`); + await editChannel(page, 'inApp'); + + feedsCheckbox = getByTestId(page, 'use-feeds-checkbox'); + await expect(feedsCheckbox).not.toBeChecked(); +}); + +test('should edit email notification', async ({ page }) => { + const template = session.templates[0]; + + await page.goto(`/workflows/edit/${template._id}`); + await editChannel(page, 'email'); + + const emailEditor = getByTestId(page, 'email-editor'); + const firstEditorRow = getByTestId(emailEditor, 'editor-row').first(); + await firstEditorRow.click(); + await firstEditorRow.press(`${modifier}+KeyA`); + await firstEditorRow.press(`${modifier}+KeyX`); + await page.keyboard.type('Hello world!'); +}); + +test('should update notification active status', async ({ page }) => { + const template = session.templates[0]; + + await page.goto(`/workflows/edit/${template._id}`); + + let settingsPage = getByTestId(page, 'settings-page'); + await settingsPage.click(); + + const toggleSwitch = getByTestId(page, 'active-toggle-switch'); + await expect(toggleSwitch).toBeVisible(); + await expect(page.getByText('Active')).toBeVisible(); + await toggleSwitch.locator('~ label').click({ force: true }); + await expect(page.getByText('Inactive')).toBeVisible(); + + await page.goto(`/workflows/edit/${template._id}`); + + settingsPage = getByTestId(page, 'settings-page'); + await settingsPage.click(); + + await expect(page.getByText('Inactive')).toBeVisible(); +}); From a7c7d4e08372241ea9128c0f891f299a5d0c7a97 Mon Sep 17 00:00:00 2001 From: Gosha Date: Mon, 8 Apr 2024 14:25:22 +0300 Subject: [PATCH 07/33] refactor(web): import echo from echo ee echo web --- .source | 2 +- apps/web/package.json | 1 + .../templates/components/InputVariables.tsx | 8 +- .../components/InputVariablesForm.tsx | 8 +- enterprise/packages/web/echo/.eslintrc.js | 43 + enterprise/packages/web/echo/.gitignore | 28 + enterprise/packages/web/echo/check-ee.mjs | 65 + enterprise/packages/web/echo/package.json | 54 + enterprise/packages/web/echo/src | 1 + .../packages/web/echo/tsconfig.esm.json | 7 + enterprise/packages/web/echo/tsconfig.json | 34 + pnpm-lock.yaml | 1583 ++++++++++++++--- pnpm-workspace.yaml | 1 + 13 files changed, 1628 insertions(+), 207 deletions(-) create mode 100644 enterprise/packages/web/echo/.eslintrc.js create mode 100644 enterprise/packages/web/echo/.gitignore create mode 100644 enterprise/packages/web/echo/check-ee.mjs create mode 100644 enterprise/packages/web/echo/package.json create mode 120000 enterprise/packages/web/echo/src create mode 100644 enterprise/packages/web/echo/tsconfig.esm.json create mode 100644 enterprise/packages/web/echo/tsconfig.json diff --git a/.source b/.source index 1bc46c93374..b6407c754b8 160000 --- a/.source +++ b/.source @@ -1 +1 @@ -Subproject commit 1bc46c933740c90ee7b0c12ad767a3671913e08e +Subproject commit b6407c754b85561409db7639eed4242a7b52e62e diff --git a/apps/web/package.json b/apps/web/package.json index 84356a43fd5..9d73b690789 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -126,6 +126,7 @@ }, "optionalDependencies": { "@novu/ee-billing-web": "^0.24.1", + "@novu/ee-echo-web": "^0.24.1", "@novu/ee-translation-web": "^0.24.1" }, "devDependencies": { diff --git a/apps/web/src/pages/templates/components/InputVariables.tsx b/apps/web/src/pages/templates/components/InputVariables.tsx index 45365e41f18..00bfc1bcc32 100644 --- a/apps/web/src/pages/templates/components/InputVariables.tsx +++ b/apps/web/src/pages/templates/components/InputVariables.tsx @@ -18,11 +18,11 @@ export const InputVariables = ({ } try { - const module = require('@novu/ee-billing-web'); + const module = require('@novu/ee-echo-web'); const InputVariablesComponent = module.InputVariables; return ; - } catch (e) {} - - return null; + } catch (e) { + throw e; + } }; diff --git a/apps/web/src/pages/templates/components/InputVariablesForm.tsx b/apps/web/src/pages/templates/components/InputVariablesForm.tsx index dff89da03d4..38a19e495a2 100644 --- a/apps/web/src/pages/templates/components/InputVariablesForm.tsx +++ b/apps/web/src/pages/templates/components/InputVariablesForm.tsx @@ -27,7 +27,7 @@ export const InputVariablesForm = ({ onChange }: { onChange?: (data: any) => voi } try { - const module = require('@novu/ee-billing-web'); + const module = require('@novu/ee-echo-web'); const InputVariablesComponent = module.InputVariablesForm; return ( @@ -40,7 +40,7 @@ export const InputVariablesForm = ({ onChange }: { onChange?: (data: any) => voi /> ); - } catch (e) {} - - return null; + } catch (e) { + throw e; + } }; diff --git a/enterprise/packages/web/echo/.eslintrc.js b/enterprise/packages/web/echo/.eslintrc.js new file mode 100644 index 00000000000..2f449acbcd9 --- /dev/null +++ b/enterprise/packages/web/echo/.eslintrc.js @@ -0,0 +1,43 @@ +module.exports = { + rules: { + 'func-names': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/no-array-index-key': 'off', + 'no-empty-pattern': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + 'react/no-unescaped-entities': 'off', + 'react/jsx-closing-bracket-location': 'off', + '@typescript-eslint/ban-types': 'off', + 'react/jsx-wrap-multilines': 'off', + 'jsx-a11y/anchor-is-valid': 'off', + 'promise/catch-or-return': 'off', + 'react/jsx-one-expression-per-line': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'jsx-a11y/aria-role': 'off', + 'jsx-a11y/no-static-element-interactions': 'off', + 'react/require-default-props': 'off', + 'react/no-danger': 'off', + 'jsx-a11y/click-events-have-key-events': 'off', + '@typescript-eslint/naming-convention': [ + 'error', + { + filter: '_', + selector: 'variableLike', + leadingUnderscore: 'allow', + format: ['PascalCase', 'camelCase', 'UPPER_CASE'], + }, + ], + '@typescript-eslint/no-empty-function': 'off', + }, + env: { + 'cypress/globals': true, + }, + ignorePatterns: ['craco.config.js', 'cypress/*'], + extends: ['plugin:cypress/recommended', '../../../.eslintrc.js'], + plugins: ['cypress'], + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2020, + sourceType: 'module', + }, +}; diff --git a/enterprise/packages/web/echo/.gitignore b/enterprise/packages/web/echo/.gitignore new file mode 100644 index 00000000000..013add19d46 --- /dev/null +++ b/enterprise/packages/web/echo/.gitignore @@ -0,0 +1,28 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + + +# production +build +dist + +.npmrc +.idea/* +.nyc_output + +test + +src/**.js +coverage +*.log +package-lock.json + +storybook-static + diff --git a/enterprise/packages/web/echo/check-ee.mjs b/enterprise/packages/web/echo/check-ee.mjs new file mode 100644 index 00000000000..0db73052ff4 --- /dev/null +++ b/enterprise/packages/web/echo/check-ee.mjs @@ -0,0 +1,65 @@ +import spawn from 'cross-spawn'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import * as fs from 'fs'; +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +const ROOT_PATH = path.resolve(dirname); +const ENCODING_TYPE = 'utf8'; +const NEW_LINE_CHAR = '\n'; + +class CliLogs { + constructor() { + this._logs = []; + this.log = this.log.bind(this); + } + + log(log) { + const cleanLog = log.trim(); + if (cleanLog.length) { + this._logs.push(cleanLog); + } + } + + get logs() { + return this._logs; + } + + get joinedLogs() { + return this.logs.join(NEW_LINE_CHAR); + } +} + +function pnpmRun(...args) { + const logData = new CliLogs(); + let pnpmProcess; + + return new Promise((resolve, reject) => { + const processOptions = { + cwd: ROOT_PATH, + env: process.env, + }; + + pnpmProcess = spawn('pnpm', args, processOptions); + + pnpmProcess.stdin.setEncoding(ENCODING_TYPE); + pnpmProcess.stdout.setEncoding(ENCODING_TYPE); + pnpmProcess.stderr.setEncoding(ENCODING_TYPE); + pnpmProcess.stdout.on('data', logData.log); + pnpmProcess.stderr.on('data', logData.log); + + pnpmProcess.on('close', (code) => { + if (code !== 0) { + reject(logData.joinedLogs); + } else { + resolve(logData.joinedLogs); + } + }); + }); +} + +const hasSrcFolder = fs.existsSync(path.resolve(ROOT_PATH, 'src')); +if (hasSrcFolder) { + await pnpmRun('build:esm'); + await pnpmRun('build:types'); +} diff --git a/enterprise/packages/web/echo/package.json b/enterprise/packages/web/echo/package.json new file mode 100644 index 00000000000..f91e2c83f97 --- /dev/null +++ b/enterprise/packages/web/echo/package.json @@ -0,0 +1,54 @@ +{ + "name": "@novu/ee-echo-web", + "version": "0.24.1", + "description": "", + "repository": "https://github.com/novuhq/novu", + "license": "ISC", + "author": "", + "private": true, + "sideEffects": false, + "module": "dist/esm/index.js", + "types": "dist/types/index.d.ts", + "files": [ + "dist/esm" + ], + "scripts": { + "prebuild": "rimraf dist", + "build": "node ./check-ee.mjs", + "build:esm": "cross-env node_modules/.bin/tsc -p tsconfig.esm.json", + "build:esm:watch": "cross-env node_modules/.bin/tsc -p tsconfig.esm.json -w --preserveWatchOutput", + "build:types": "tsc --declaration --emitDeclarationOnly --declarationMap --declarationDir dist/types -p tsconfig.json", + "build:watch": "npm run build:esm:watch", + "lint": "eslint --ext .ts,.tsx src --no-error-on-unmatched-pattern", + "test": "echo 'skip test in the ci'", + "start": "npm run build:watch" + }, + "dependencies": { + "@mantine/core": "^5.7.1", + "@mantine/hooks": "^5.7.1", + "@novu/design-system": "^0.24.1", + "@novu/shared-web": "^0.24.1", + "@rjsf/core": "^5.17.1", + "@rjsf/validator-ajv8": "^5.17.1", + "@tanstack/react-query": "^4.20.4", + "eslint": "^8.33.0", + "eslint-plugin-react-hooks": "^4.4.0", + "react-router-dom": "6.2.2", + "tslib": "^2.3.1" + }, + "devDependencies": { + "@types/node": "^18.11.12", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "rimraf": "^3.0.2", + "ts-loader": "~9.4.0", + "tslib": "^2.3.1", + "typescript": "4.9.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } +} diff --git a/enterprise/packages/web/echo/src b/enterprise/packages/web/echo/src new file mode 120000 index 00000000000..7edcb0e30e4 --- /dev/null +++ b/enterprise/packages/web/echo/src @@ -0,0 +1 @@ +../../../../.source/web/echo/src \ No newline at end of file diff --git a/enterprise/packages/web/echo/tsconfig.esm.json b/enterprise/packages/web/echo/tsconfig.esm.json new file mode 100644 index 00000000000..48e68b0620b --- /dev/null +++ b/enterprise/packages/web/echo/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "outDir": "./dist/esm" + } +} diff --git a/enterprise/packages/web/echo/tsconfig.json b/enterprise/packages/web/echo/tsconfig.json new file mode 100644 index 00000000000..59907bb89b1 --- /dev/null +++ b/enterprise/packages/web/echo/tsconfig.json @@ -0,0 +1,34 @@ +{ + "include": [ + "src" + ], + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist/cjs", + "forceConsistentCasingInFileNames": true, + "target": "es6", + "strict": true, + "typeRoots": [ + "./node_modules/@types" + ], + "jsx": "react", + "module": "commonjs", + "lib": [ + "ESNext", + "dom" + ], + "skipLibCheck": true, + "declaration": false, + "declarationMap": false, + "sourceMap": true, + "removeComments": false, + "allowSyntheticDefaultImports": true, + "baseUrl": "." + }, + "exclude": [ + "src/**/*.test.*", + "src/*.test.*", + "node_modules", + "**/node_modules/*" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1c025d869c..c9e8f1dced6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,7 +233,7 @@ importers: version: 6.1.13 ts-jest: specifier: 27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@16.11.7)(typescript@4.9.5) @@ -608,7 +608,7 @@ importers: version: 9.2.4 ts-jest: specifier: ^27.0.7 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-loader: specifier: ~9.4.0 version: 9.4.2(typescript@4.9.5)(webpack@5.78.0) @@ -629,13 +629,13 @@ importers: version: 4.6.2(react-dom@17.0.2)(react@17.0.2) '@babel/plugin-proposal-optional-chaining': specifier: ^7.20.7 - version: 7.21.0(@babel/core@7.23.2) + version: 7.21.0(@babel/core@7.24.4) '@babel/plugin-transform-react-display-name': specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.23.2) + version: 7.18.6(@babel/core@7.24.4) '@babel/plugin-transform-runtime': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@cypress/react': specifier: ^7.0.3 version: 7.0.3(@types/react@17.0.53)(cypress@13.3.1)(react-dom@17.0.2)(react@17.0.2) @@ -916,6 +916,9 @@ importers: '@novu/ee-billing-web': specifier: ^0.24.1 version: link:../../enterprise/packages/billing-web + '@novu/ee-echo-web': + specifier: ^0.24.1 + version: link:../../enterprise/packages/web/echo '@novu/ee-translation-web': specifier: ^0.24.1 version: link:../../enterprise/packages/translation-web @@ -925,13 +928,13 @@ importers: version: 7.12.1 '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/preset-react': specifier: ^7.13.13 - version: 7.18.6(@babel/core@7.23.2) + version: 7.18.6(@babel/core@7.24.4) '@babel/preset-typescript': specifier: ^7.13.0 - version: 7.21.4(@babel/core@7.23.2) + version: 7.21.4(@babel/core@7.24.4) '@babel/runtime': specifier: ^7.20.13 version: 7.21.0 @@ -961,13 +964,13 @@ importers: version: 7.4.2 '@storybook/preset-create-react-app': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.23.2)(react-refresh@0.14.0)(react-scripts@5.0.1)(typescript@4.9.5)(webpack-dev-server@4.11.1)(webpack@5.78.0) + version: 7.4.2(@babel/core@7.24.4)(react-refresh@0.14.0)(react-scripts@5.0.1)(typescript@4.9.5)(webpack-dev-server@4.11.1)(webpack@5.78.0) '@storybook/react': specifier: ^7.4.2 version: 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@storybook/react-webpack5': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1) + version: 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1) '@testing-library/jest-dom': specifier: ^4.2.4 version: 4.2.4 @@ -1154,7 +1157,7 @@ importers: version: 6.3.3 ts-jest: specifier: ^27.0.7 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-loader: specifier: ~9.4.0 version: 9.4.2(typescript@4.9.5)(webpack@5.78.0) @@ -1181,7 +1184,7 @@ importers: version: 11.10.6(@emotion/react@11.10.6)(@types/react@17.0.62)(react@17.0.2) '@mantine/core': specifier: 4.2.12 - version: 4.2.12(@babel/core@7.23.2)(@mantine/hooks@4.2.12)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) + version: 4.2.12(@babel/core@7.24.4)(@mantine/hooks@4.2.12)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) '@mantine/hooks': specifier: 4.2.12 version: 4.2.12(react@17.0.2) @@ -1248,25 +1251,25 @@ importers: devDependencies: '@babel/plugin-proposal-optional-chaining': specifier: ^7.20.7 - version: 7.21.0(@babel/core@7.23.2) + version: 7.21.0(@babel/core@7.24.4) '@babel/plugin-transform-react-display-name': specifier: ^7.18.6 - version: 7.22.5(@babel/core@7.23.2) + version: 7.22.5(@babel/core@7.24.4) '@babel/plugin-transform-runtime': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/polyfill': specifier: ^7.12.1 version: 7.12.1 '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/preset-react': specifier: ^7.13.13 - version: 7.22.15(@babel/core@7.23.2) + version: 7.22.15(@babel/core@7.24.4) '@babel/preset-typescript': specifier: ^7.13.0 - version: 7.21.4(@babel/core@7.23.2) + version: 7.21.4(@babel/core@7.24.4) '@babel/runtime': specifier: ^7.20.13 version: 7.21.0 @@ -2341,6 +2344,67 @@ importers: specifier: 4.9.5 version: 4.9.5 + enterprise/packages/web/echo: + dependencies: + '@mantine/core': + specifier: ^5.7.1 + version: 5.10.5(@emotion/react@11.10.6)(@mantine/hooks@5.10.5)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) + '@mantine/hooks': + specifier: ^5.7.1 + version: 5.10.5(react@17.0.2) + '@novu/design-system': + specifier: ^0.24.1 + version: link:../../../../libs/design-system + '@novu/shared-web': + specifier: ^0.24.1 + version: link:../../../../libs/shared-web + '@rjsf/core': + specifier: ^5.17.1 + version: 5.17.1(@rjsf/utils@5.17.1)(react@17.0.2) + '@rjsf/validator-ajv8': + specifier: ^5.17.1 + version: 5.17.1(@rjsf/utils@5.17.1) + '@tanstack/react-query': + specifier: ^4.20.4 + version: 4.29.1(react-dom@17.0.2)(react@17.0.2) + eslint: + specifier: ^8.33.0 + version: 8.51.0 + eslint-plugin-react-hooks: + specifier: ^4.4.0 + version: 4.6.0(eslint@8.51.0) + react-router-dom: + specifier: 6.2.2 + version: 6.2.2(react-dom@17.0.2)(react@17.0.2) + tslib: + specifier: ^2.3.1 + version: 2.6.2 + devDependencies: + '@types/node': + specifier: ^18.11.12 + version: 18.18.5 + '@types/react': + specifier: ^17.0.0 + version: 17.0.62 + '@types/react-dom': + specifier: ^17.0.0 + version: 17.0.20 + react: + specifier: ^17.0.1 + version: 17.0.2 + react-dom: + specifier: ^17.0.1 + version: 17.0.2(react@17.0.2) + rimraf: + specifier: ^3.0.2 + version: 3.0.2 + ts-loader: + specifier: ~9.4.0 + version: 9.4.2(typescript@4.9.5)(webpack@5.78.0) + typescript: + specifier: 4.9.5 + version: 4.9.5 + libs/dal: dependencies: '@aws-sdk/client-s3': @@ -2527,7 +2591,7 @@ importers: version: 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@storybook/react-webpack5': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) + version: 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@storybook/theming': specifier: ^7.4.2 version: 7.4.2(react-dom@17.0.2)(react@17.0.2) @@ -2699,7 +2763,7 @@ importers: version: 0.8.5 ts-jest: specifier: ^27.1.3 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -3313,7 +3377,7 @@ importers: version: 9.2.4 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -3414,7 +3478,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -3439,10 +3503,10 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/preset-typescript': specifier: ^7.13.0 - version: 7.21.4(@babel/core@7.23.2) + version: 7.21.4(@babel/core@7.24.4) '@types/jest': specifier: ^29.2.3 version: 29.5.0 @@ -3457,7 +3521,7 @@ importers: version: 29.5.0 ts-jest: specifier: ^29.0.3 - version: 29.1.0(@babel/core@7.23.2)(jest@29.5.0)(typescript@4.9.5) + version: 29.1.0(@babel/core@7.24.4)(jest@29.5.0)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -3509,7 +3573,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -3582,7 +3646,7 @@ importers: version: 0.0.0 ts-jest: specifier: ^29.1.2 - version: 29.1.2(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.5) + version: 29.1.2(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -3646,13 +3710,13 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/preset-react': specifier: ^7.13.13 - version: 7.22.15(@babel/core@7.23.2) + version: 7.22.15(@babel/core@7.24.4) '@babel/preset-typescript': specifier: ^7.13.0 - version: 7.21.4(@babel/core@7.23.2) + version: 7.21.4(@babel/core@7.24.4) '@storybook/addon-actions': specifier: ^7.4.2 version: 7.4.2(@types/react-dom@17.0.19)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) @@ -3673,7 +3737,7 @@ importers: version: 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@storybook/react-webpack5': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) + version: 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) '@testing-library/dom': specifier: ^9.3.0 version: 9.3.0 @@ -3706,7 +3770,7 @@ importers: version: 8.8.2 babel-loader: specifier: ^8.2.4 - version: 8.3.0(@babel/core@7.23.2)(webpack@5.82.1) + version: 8.3.0(@babel/core@7.24.4)(webpack@5.82.1) compression-webpack-plugin: specifier: ^10.0.0 version: 10.0.0(webpack@5.82.1) @@ -3739,7 +3803,7 @@ importers: version: 5.3.9(@swc/core@1.3.49)(esbuild@0.18.20)(webpack@5.82.1) ts-jest: specifier: ^29.0.3 - version: 29.1.0(@babel/core@7.23.2)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5) + version: 29.1.0(@babel/core@7.24.4)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5) ts-loader: specifier: ~9.4.0 version: 9.4.2(typescript@4.9.5)(webpack@5.82.1) @@ -3973,7 +4037,7 @@ importers: version: 0.0.0 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -4016,7 +4080,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4065,7 +4129,7 @@ importers: version: 2.8.7 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4111,7 +4175,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4154,7 +4218,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4197,7 +4261,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4246,7 +4310,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4289,7 +4353,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4344,7 +4408,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4399,7 +4463,7 @@ importers: version: 2.8.7 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4445,7 +4509,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4497,7 +4561,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4543,7 +4607,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4595,7 +4659,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4650,7 +4714,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4714,7 +4778,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4736,7 +4800,7 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 version: 1.0.2(nyc@15.1.0) @@ -4778,7 +4842,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4824,7 +4888,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4867,7 +4931,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4910,7 +4974,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4956,7 +5020,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4978,7 +5042,7 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 version: 1.0.2(nyc@15.1.0) @@ -5014,7 +5078,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5069,7 +5133,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5115,7 +5179,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5167,7 +5231,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5222,7 +5286,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5283,7 +5347,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5341,7 +5405,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5387,7 +5451,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5445,7 +5509,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5497,7 +5561,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5540,7 +5604,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5583,7 +5647,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5635,7 +5699,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5693,7 +5757,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.7 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5751,7 +5815,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5809,7 +5873,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.7 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5855,7 +5919,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5910,7 +5974,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5965,7 +6029,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6011,7 +6075,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6066,7 +6130,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6112,7 +6176,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6155,7 +6219,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6198,7 +6262,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6250,7 +6314,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6296,7 +6360,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6339,7 +6403,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6382,7 +6446,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6425,7 +6489,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6477,7 +6541,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6532,7 +6596,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6590,7 +6654,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6636,7 +6700,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6688,7 +6752,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6734,7 +6798,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6756,7 +6820,7 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 version: 1.0.2(nyc@15.1.0) @@ -6792,7 +6856,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6847,7 +6911,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6905,7 +6969,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6960,7 +7024,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6982,7 +7046,7 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 version: 1.0.2(nyc@15.1.0) @@ -7018,7 +7082,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -7073,7 +7137,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -7119,7 +7183,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -9908,7 +9972,6 @@ packages: dependencies: '@babel/highlight': 7.24.2 picocolors: 1.0.0 - dev: true /@babel/compat-data@7.23.2: resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} @@ -9917,7 +9980,6 @@ packages: /@babel/compat-data@7.24.4: resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/core@7.21.4: resolution: {integrity: sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==} @@ -10031,7 +10093,6 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color - dev: true /@babel/eslint-parser@7.21.3(@babel/core@7.23.2)(eslint@8.51.0): resolution: {integrity: sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==} @@ -10074,7 +10135,6 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 - dev: true /@babel/helper-annotate-as-pure@7.18.6: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} @@ -10116,7 +10176,6 @@ packages: browserslist: 4.23.0 lru-cache: 5.1.1 semver: 6.3.1 - dev: true /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.21.4): resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} @@ -10190,6 +10249,24 @@ packages: semver: 6.3.1 dev: true + /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.22.15 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} engines: {node: '>=6.9.0'} @@ -10226,6 +10303,18 @@ packages: semver: 6.3.1 dev: true + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.22.11): resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} peerDependencies: @@ -10269,6 +10358,21 @@ packages: resolve: 1.22.2 transitivePeerDependencies: - supports-color + dev: true + + /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.24.4): + resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + debug: 4.3.4(supports-color@8.1.1) + lodash.debounce: 4.0.8 + resolve: 1.22.2 + transitivePeerDependencies: + - supports-color /@babel/helper-environment-visitor@7.22.20: resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} @@ -10334,6 +10438,20 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true + /@babel/helper-module-transforms@7.22.20(@babel/core@7.24.4): + resolution: {integrity: sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/helper-module-transforms@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} engines: {node: '>=6.9.0'} @@ -10375,6 +10493,20 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 + /@babel/helper-module-transforms@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} engines: {node: '>=6.9.0'} @@ -10387,7 +10519,6 @@ packages: '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 - dev: true /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} @@ -10440,6 +10571,18 @@ packages: '@babel/helper-wrap-function': 7.22.20 dev: true + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.4): + resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-wrap-function': 7.22.20 + dev: true + /@babel/helper-replace-supers@7.22.20(@babel/core@7.21.4): resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} engines: {node: '>=6.9.0'} @@ -10488,6 +10631,18 @@ packages: '@babel/helper-optimise-call-expression': 7.22.5 dev: true + /@babel/helper-replace-supers@7.22.20(@babel/core@7.24.4): + resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-member-expression-to-functions': 7.22.15 + '@babel/helper-optimise-call-expression': 7.22.5 + dev: true + /@babel/helper-simple-access@7.22.5: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} @@ -10513,7 +10668,6 @@ packages: /@babel/helper-string-parser@7.24.1: resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} @@ -10531,7 +10685,6 @@ packages: /@babel/helper-validator-option@7.23.5: resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-wrap-function@7.22.20: resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} @@ -10572,7 +10725,6 @@ packages: '@babel/types': 7.24.0 transitivePeerDependencies: - supports-color - dev: true /@babel/highlight@7.22.13: resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} @@ -10590,7 +10742,6 @@ packages: chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.0.0 - dev: true /@babel/parser@7.22.16: resolution: {integrity: sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==} @@ -10619,7 +10770,6 @@ packages: hasBin: true dependencies: '@babel/types': 7.24.0 - dev: true /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==} @@ -10651,6 +10801,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==} engines: {node: '>=6.9.0'} @@ -10687,6 +10847,18 @@ packages: '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.23.2) dev: true + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.24.4) + dev: true + /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.22.9): resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} @@ -10771,6 +10943,18 @@ packages: '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.2) + dev: true + + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.24.4): + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.4) /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.23.2): resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} @@ -10824,6 +11008,15 @@ packages: '@babel/core': 7.23.2 dev: true + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.4): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: true + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.22.9): resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} @@ -10872,6 +11065,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.4): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -10926,6 +11128,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.4): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.22.11): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} @@ -10956,6 +11167,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.4): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-decorators@7.21.0(@babel/core@7.23.2): resolution: {integrity: sha512-tIoPpGBR8UuM4++ccWN3gifhVvQu7ZizuR1fklhRJrd5ewgbkUS+0KVFeWWxELtn18NTLoW32XV7zyOgIAiz+w==} engines: {node: '>=6.9.0'} @@ -11003,6 +11224,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.22.11): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: @@ -11030,6 +11260,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-flow@7.22.5(@babel/core@7.23.2): resolution: {integrity: sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==} engines: {node: '>=6.9.0'} @@ -11040,6 +11279,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-flow@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} engines: {node: '>=6.9.0'} @@ -11070,6 +11319,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==} engines: {node: '>=6.9.0'} @@ -11100,6 +11359,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.4): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -11136,6 +11405,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.4): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -11172,6 +11450,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.21.4): resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} engines: {node: '>=6.9.0'} @@ -11192,6 +11479,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.4): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -11228,6 +11525,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.4): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -11264,6 +11570,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.4): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -11300,6 +11615,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.4): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -11336,6 +11660,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -11372,6 +11705,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -11406,6 +11748,15 @@ packages: dependencies: '@babel/core': 7.23.2 '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.22.11): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} @@ -11437,6 +11788,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.4): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.4): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} @@ -11477,13 +11838,23 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-typescript@7.21.4(@babel/core@7.23.2): + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.4): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-typescript@7.21.4(@babel/core@7.24.4): resolution: {integrity: sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -11540,6 +11911,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.4): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} engines: {node: '>=6.9.0'} @@ -11570,6 +11952,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-async-generator-functions@7.23.2(@babel/core@7.22.11): resolution: {integrity: sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==} engines: {node: '>=6.9.0'} @@ -11609,6 +12001,19 @@ packages: '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-async-generator-functions@7.23.2(@babel/core@7.24.4): + resolution: {integrity: sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.4) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} engines: {node: '>=6.9.0'} @@ -11645,6 +12050,18 @@ packages: '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} engines: {node: '>=6.9.0'} @@ -11675,6 +12092,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-block-scoping@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==} engines: {node: '>=6.9.0'} @@ -11705,6 +12132,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-block-scoping@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==} engines: {node: '>=6.9.0'} @@ -11738,6 +12175,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-class-static-block@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==} engines: {node: '>=6.9.0'} @@ -11774,6 +12222,18 @@ packages: '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-class-static-block@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-classes@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==} engines: {node: '>=6.9.0'} @@ -11828,6 +12288,24 @@ packages: globals: 11.12.0 dev: true + /@babel/plugin-transform-classes@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.4) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + dev: true + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} engines: {node: '>=6.9.0'} @@ -11861,6 +12339,17 @@ packages: '@babel/template': 7.22.15 dev: true + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.15 + dev: true + /@babel/plugin-transform-destructuring@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==} engines: {node: '>=6.9.0'} @@ -11891,6 +12380,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-destructuring@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} engines: {node: '>=6.9.0'} @@ -11924,6 +12423,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} engines: {node: '>=6.9.0'} @@ -11954,6 +12464,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-dynamic-import@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==} engines: {node: '>=6.9.0'} @@ -11987,6 +12507,17 @@ packages: '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-dynamic-import@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} engines: {node: '>=6.9.0'} @@ -12020,6 +12551,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-export-namespace-from@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==} engines: {node: '>=6.9.0'} @@ -12053,6 +12595,17 @@ packages: '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-export-namespace-from@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-flow-strip-types@7.22.5(@babel/core@7.23.2): resolution: {integrity: sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA==} engines: {node: '>=6.9.0'} @@ -12064,6 +12617,17 @@ packages: '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-flow-strip-types@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-for-of@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==} engines: {node: '>=6.9.0'} @@ -12094,6 +12658,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-for-of@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} engines: {node: '>=6.9.0'} @@ -12130,6 +12704,18 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-json-strings@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==} engines: {node: '>=6.9.0'} @@ -12163,6 +12749,17 @@ packages: '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-json-strings@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-literals@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} engines: {node: '>=6.9.0'} @@ -12193,6 +12790,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-literals@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-logical-assignment-operators@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==} engines: {node: '>=6.9.0'} @@ -12226,6 +12833,17 @@ packages: '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-logical-assignment-operators@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} engines: {node: '>=6.9.0'} @@ -12256,6 +12874,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-modules-amd@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==} engines: {node: '>=6.9.0'} @@ -12289,6 +12917,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-modules-amd@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-modules-commonjs@7.22.15(@babel/core@7.23.2): resolution: {integrity: sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==} engines: {node: '>=6.9.0'} @@ -12301,6 +12940,18 @@ packages: '@babel/helper-simple-access': 7.22.5 dev: true + /@babel/plugin-transform-modules-commonjs@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-transforms': 7.22.20(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + /@babel/plugin-transform-modules-commonjs@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==} engines: {node: '>=6.9.0'} @@ -12337,6 +12988,18 @@ packages: '@babel/helper-simple-access': 7.22.5 dev: true + /@babel/plugin-transform-modules-commonjs@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + /@babel/plugin-transform-modules-systemjs@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==} engines: {node: '>=6.9.0'} @@ -12376,6 +13039,19 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true + /@babel/plugin-transform-modules-systemjs@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} engines: {node: '>=6.9.0'} @@ -12409,6 +13085,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} @@ -12442,6 +13129,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} engines: {node: '>=6.9.0'} @@ -12472,6 +13170,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-nullish-coalescing-operator@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==} engines: {node: '>=6.9.0'} @@ -12505,6 +13213,17 @@ packages: '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-nullish-coalescing-operator@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-numeric-separator@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==} engines: {node: '>=6.9.0'} @@ -12538,6 +13257,17 @@ packages: '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-numeric-separator@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-object-rest-spread@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==} engines: {node: '>=6.9.0'} @@ -12580,6 +13310,20 @@ packages: '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-object-rest-spread@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.2 + '@babel/core': 7.24.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} engines: {node: '>=6.9.0'} @@ -12613,6 +13357,17 @@ packages: '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-optional-catch-binding@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==} engines: {node: '>=6.9.0'} @@ -12646,6 +13401,17 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-optional-catch-binding@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-optional-chaining@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==} engines: {node: '>=6.9.0'} @@ -12682,6 +13448,18 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-optional-chaining@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-parameters@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==} engines: {node: '>=6.9.0'} @@ -12712,6 +13490,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-parameters@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==} engines: {node: '>=6.9.0'} @@ -12745,6 +13533,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-private-property-in-object@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==} engines: {node: '>=6.9.0'} @@ -12784,6 +13583,19 @@ packages: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-private-property-in-object@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} engines: {node: '>=6.9.0'} @@ -12814,6 +13626,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-react-constant-elements@7.21.3(@babel/core@7.23.2): resolution: {integrity: sha512-4DVcFeWe/yDYBLp0kBmOGFJ6N2UYg7coGid1gdxb4co62dy/xISDMaYBXBVXEDhfgMk7qkbcYiGtwd5Q/hwDDQ==} engines: {node: '>=6.9.0'} @@ -12824,13 +13646,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.23.2): + /@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-transform-react-display-name@7.22.5(@babel/core@7.23.2): @@ -12843,14 +13665,24 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.23.2): + /@babel/plugin-transform-react-display-name@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 - '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.23.2) + '@babel/core': 7.24.4 + '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.24.4) dev: true /@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.23.2): @@ -12863,6 +13695,16 @@ packages: '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.23.2): resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==} engines: {node: '>=6.9.0'} @@ -12883,17 +13725,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-jsx@7.21.0(@babel/core@7.23.2): + /@babel/plugin-transform-react-jsx@7.21.0(@babel/core@7.24.4): resolution: {integrity: sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-module-imports': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.2) + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.4) '@babel/types': 7.22.19 dev: true @@ -12911,13 +13753,27 @@ packages: '@babel/types': 7.23.0 dev: true - /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.23.2): + /@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.4) + '@babel/types': 7.23.0 + dev: true + + /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -12933,6 +13789,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-react-pure-annotations@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.22.11): resolution: {integrity: sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==} engines: {node: '>=6.9.0'} @@ -12966,6 +13833,17 @@ packages: regenerator-transform: 0.15.2 dev: true + /@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.24.4): + resolution: {integrity: sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + regenerator-transform: 0.15.2 + dev: true + /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} engines: {node: '>=6.9.0'} @@ -12996,6 +13874,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-runtime@7.22.9(@babel/core@7.22.9): resolution: {integrity: sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ==} engines: {node: '>=6.9.0'} @@ -13028,6 +13916,23 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: true + + /@babel/plugin-transform-runtime@7.23.2(@babel/core@7.24.4): + resolution: {integrity: sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.24.4) + babel-plugin-polyfill-corejs3: 0.8.5(@babel/core@7.24.4) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.24.4) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} @@ -13059,6 +13964,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-spread@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} engines: {node: '>=6.9.0'} @@ -13092,6 +14007,17 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true + /@babel/plugin-transform-spread@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true + /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} engines: {node: '>=6.9.0'} @@ -13122,6 +14048,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} engines: {node: '>=6.9.0'} @@ -13152,6 +14088,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} engines: {node: '>=6.9.0'} @@ -13182,6 +14128,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-typescript@7.21.3(@babel/core@7.21.4): resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} engines: {node: '>=6.9.0'} @@ -13205,7 +14161,20 @@ packages: '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.2) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.23.2) + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.23.2) + dev: true + + /@babel/plugin-transform-typescript@7.21.3(@babel/core@7.24.4): + resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.24.4) dev: true /@babel/plugin-transform-typescript@7.22.15(@babel/core@7.23.2): @@ -13251,6 +14220,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-unicode-escapes@7.22.10(@babel/core@7.24.4): + resolution: {integrity: sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==} engines: {node: '>=6.9.0'} @@ -13284,6 +14263,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} engines: {node: '>=6.9.0'} @@ -13317,6 +14307,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==} engines: {node: '>=6.9.0'} @@ -13350,6 +14351,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/polyfill@7.12.1: resolution: {integrity: sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==} deprecated: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information. @@ -13631,6 +14643,97 @@ packages: - supports-color dev: true + /@babel/preset-env@7.23.2(@babel/core@7.24.4): + resolution: {integrity: sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.2 + '@babel/core': 7.24.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.4) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.4) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.4) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.4) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.4) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.4) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.4) + '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-async-generator-functions': 7.23.2(@babel/core@7.24.4) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-block-scoping': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-class-static-block': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-classes': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-destructuring': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-dynamic-import': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-export-namespace-from': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-for-of': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-json-strings': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-logical-assignment-operators': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-modules-amd': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-modules-commonjs': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-modules-systemjs': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-numeric-separator': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-object-rest-spread': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-optional-catch-binding': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-private-property-in-object': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-regenerator': 7.22.10(@babel/core@7.24.4) + '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-unicode-escapes': 7.22.10(@babel/core@7.24.4) + '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.24.4) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.4) + '@babel/types': 7.23.0 + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.24.4) + babel-plugin-polyfill-corejs3: 0.8.5(@babel/core@7.24.4) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.24.4) + core-js-compat: 3.32.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/preset-flow@7.22.15(@babel/core@7.23.2): resolution: {integrity: sha512-dB5aIMqpkgbTfN5vDdTRPzjqtWiZcRESNR88QYnoPR+bmdYoluOzMX9tQerTv0XzSgZYctPfO1oc0N5zdog1ew==} engines: {node: '>=6.9.0'} @@ -13643,6 +14746,18 @@ packages: '@babel/plugin-transform-flow-strip-types': 7.22.5(@babel/core@7.23.2) dev: true + /@babel/preset-flow@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-dB5aIMqpkgbTfN5vDdTRPzjqtWiZcRESNR88QYnoPR+bmdYoluOzMX9tQerTv0XzSgZYctPfO1oc0N5zdog1ew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-transform-flow-strip-types': 7.22.5(@babel/core@7.24.4) + dev: true + /@babel/preset-modules@0.1.5(@babel/core@7.22.9): resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} peerDependencies: @@ -13678,19 +14793,30 @@ packages: esutils: 2.0.3 dev: true - /@babel/preset-react@7.18.6(@babel/core@7.23.2): + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.4): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/types': 7.23.0 + esutils: 2.0.3 + dev: true + + /@babel/preset-react@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.23.2) - '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.23.2) - '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.23.2) - '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.23.2) + '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.24.4) + '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.24.4) dev: true /@babel/preset-react@7.22.15(@babel/core@7.23.2): @@ -13708,6 +14834,21 @@ packages: '@babel/plugin-transform-react-pure-annotations': 7.22.5(@babel/core@7.23.2) dev: true + /@babel/preset-react@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-transform-react-display-name': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-react-pure-annotations': 7.22.5(@babel/core@7.24.4) + dev: true + /@babel/preset-typescript@7.21.4(@babel/core@7.23.2): resolution: {integrity: sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A==} engines: {node: '>=6.9.0'} @@ -13722,6 +14863,20 @@ packages: '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.23.2) dev: true + /@babel/preset-typescript@7.21.4(@babel/core@7.24.4): + resolution: {integrity: sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-modules-commonjs': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.24.4) + dev: true + /@babel/preset-typescript@7.23.2(@babel/core@7.23.2): resolution: {integrity: sha512-u4UJc1XsS1GhIGteM8rnGiIvf9rJpiVgMEeCnwlLA7WJPC+jcXWJAGxYmeqs5hOZD8BbAfnV5ezBOxQbb4OUxA==} engines: {node: '>=6.9.0'} @@ -13804,7 +14959,6 @@ packages: '@babel/code-frame': 7.24.2 '@babel/parser': 7.24.4 '@babel/types': 7.24.0 - dev: true /@babel/traverse@7.23.2: resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} @@ -13839,7 +14993,6 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: true /@babel/types@7.22.19: resolution: {integrity: sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==} @@ -13864,7 +15017,6 @@ packages: '@babel/helper-string-parser': 7.24.1 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - dev: true /@bandwidth/messaging@4.1.3: resolution: {integrity: sha512-cc1qLocHGxxqV7YNGOBxt6VhO+iGLfZnIq2htMP/xCgGOHqCtOVqHlQs80AETIMNEClXapShvn4TQrakx2h1/A==} @@ -16301,7 +17453,7 @@ packages: react: 17.0.2 dev: false - /@emotion/react@11.7.1(@babel/core@7.23.2)(@types/react@17.0.62)(react@17.0.2): + /@emotion/react@11.7.1(@babel/core@7.24.4)(@types/react@17.0.62)(react@17.0.2): resolution: {integrity: sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw==} peerDependencies: '@babel/core': ^7.0.0 @@ -16313,7 +17465,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/runtime': 7.23.2 '@emotion/cache': 11.10.7 '@emotion/serialize': 1.1.1 @@ -18596,7 +19748,6 @@ packages: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.25 - dev: true /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} @@ -18613,7 +19764,6 @@ packages: /@jridgewell/set-array@1.2.1: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - dev: true /@jridgewell/source-map@0.3.3: resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} @@ -18644,7 +19794,6 @@ packages: dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - dev: true /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -19423,7 +20572,7 @@ packages: - debug dev: false - /@mantine/core@4.2.12(@babel/core@7.23.2)(@mantine/hooks@4.2.12)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2): + /@mantine/core@4.2.12(@babel/core@7.24.4)(@mantine/hooks@4.2.12)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2): resolution: {integrity: sha512-PZcVUvcSZiZmLR1moKBJFdFIh6a4C+TE2ao91kzTAlH5Qb8t/V3ONbfPk3swHoYr7OSLJQM8vZ7UD5sFDiq0/g==} peerDependencies: '@mantine/hooks': 4.2.12 @@ -19431,7 +20580,7 @@ packages: react-dom: '>=16.8.0' dependencies: '@mantine/hooks': 4.2.12(react@17.0.2) - '@mantine/styles': 4.2.12(@babel/core@7.23.2)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) + '@mantine/styles': 4.2.12(@babel/core@7.24.4)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) '@popperjs/core': 2.11.7 '@radix-ui/react-scroll-area': 0.1.4(react@17.0.2) react: 17.0.2 @@ -19572,14 +20721,14 @@ packages: react-dom: 17.0.2(react@17.0.2) dev: false - /@mantine/styles@4.2.12(@babel/core@7.23.2)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2): + /@mantine/styles@4.2.12(@babel/core@7.24.4)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2): resolution: {integrity: sha512-9q1DzW0UNW/ORMGLHfN2XABOSEm0ZQebhNlLD757R6OQouoLuUf9elUwgGOXSyogMlsAYoy84XbJ3ZbbTm4YCA==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: '@emotion/cache': 11.7.1 - '@emotion/react': 11.7.1(@babel/core@7.23.2)(@types/react@17.0.62)(react@17.0.2) + '@emotion/react': 11.7.1(@babel/core@7.24.4)(@types/react@17.0.62)(react@17.0.2) '@emotion/serialize': 1.0.2 '@emotion/utils': 1.0.0 clsx: 1.2.1 @@ -29331,13 +30480,13 @@ packages: /@storybook/postinstall@7.4.2: resolution: {integrity: sha512-L9r14KqS87HPyXw0S3pK2X29ckel/4sdBSmy9nVF8n/ADafKE0pSLKB935VL0+88eMx06aT32SMcQoqjubGKWw==} - /@storybook/preset-create-react-app@7.4.2(@babel/core@7.23.2)(react-refresh@0.14.0)(react-scripts@5.0.1)(typescript@4.9.5)(webpack-dev-server@4.11.1)(webpack@5.78.0): + /@storybook/preset-create-react-app@7.4.2(@babel/core@7.24.4)(react-refresh@0.14.0)(react-scripts@5.0.1)(typescript@4.9.5)(webpack-dev-server@4.11.1)(webpack@5.78.0): resolution: {integrity: sha512-rHRaiWmNAFXVHlRBG4iQE0Vsg3n4ZUyRWqddV2NuqZnHYQYUP07Rp0c3TFigGeTqF/gNbj8rTBDawcwpc8VkqQ==} peerDependencies: '@babel/core': '*' react-scripts: '>=5.0.0' dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.14.0)(webpack-dev-server@4.11.1)(webpack@5.78.0) '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@4.9.5)(webpack@5.78.0) '@storybook/types': 7.4.2 @@ -29359,7 +30508,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/preset-react-webpack@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4): + /@storybook/preset-react-webpack@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4): resolution: {integrity: sha512-CWWiwZa3/0zHnc6zLvI9Sgj12gJDTktZO87/gfwq2VfbWqAEUYsKs6NE4Pm0Yg9O4/IG8DHoHIB+bTNlLp/lCA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -29373,9 +30522,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 - '@babel/preset-flow': 7.22.15(@babel/core@7.23.2) - '@babel/preset-react': 7.22.15(@babel/core@7.23.2) + '@babel/core': 7.24.4 + '@babel/preset-flow': 7.22.15(@babel/core@7.24.4) + '@babel/preset-react': 7.22.15(@babel/core@7.24.4) '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.11.0)(webpack-dev-server@4.11.1)(webpack@5.78.0) '@storybook/core-webpack': 7.4.2 '@storybook/docs-tools': 7.4.2 @@ -29408,7 +30557,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/preset-react-webpack@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1): + /@storybook/preset-react-webpack@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1): resolution: {integrity: sha512-CWWiwZa3/0zHnc6zLvI9Sgj12gJDTktZO87/gfwq2VfbWqAEUYsKs6NE4Pm0Yg9O4/IG8DHoHIB+bTNlLp/lCA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -29422,9 +30571,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 - '@babel/preset-flow': 7.22.15(@babel/core@7.23.2) - '@babel/preset-react': 7.22.15(@babel/core@7.23.2) + '@babel/core': 7.24.4 + '@babel/preset-flow': 7.22.15(@babel/core@7.24.4) + '@babel/preset-react': 7.22.15(@babel/core@7.24.4) '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.11.0)(webpack-dev-server@4.11.1)(webpack@5.78.0) '@storybook/core-webpack': 7.4.2 '@storybook/docs-tools': 7.4.2 @@ -29545,7 +30694,7 @@ packages: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - /@storybook/react-webpack5@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4): + /@storybook/react-webpack5@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4): resolution: {integrity: sha512-pnl11MYKM3jRmHQz2dSnEDfDiApdH7ys3zH/FjImsTK6S8etMKlxGnZ58Puxj05qvrBRgpxnQSL+ZazfrEX/6w==} engines: {node: '>=16.0.0'} peerDependencies: @@ -29559,9 +30708,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@storybook/builder-webpack5': 7.4.2(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) - '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) + '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) '@storybook/react': 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@types/node': 16.11.7 react: 17.0.2 @@ -29585,7 +30734,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/react-webpack5@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1): + /@storybook/react-webpack5@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1): resolution: {integrity: sha512-pnl11MYKM3jRmHQz2dSnEDfDiApdH7ys3zH/FjImsTK6S8etMKlxGnZ58Puxj05qvrBRgpxnQSL+ZazfrEX/6w==} engines: {node: '>=16.0.0'} peerDependencies: @@ -29599,9 +30748,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@storybook/builder-webpack5': 7.4.2(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) - '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1) + '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1) '@storybook/react': 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@types/node': 16.11.7 react: 17.0.2 @@ -29625,7 +30774,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/react-webpack5@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5): + /@storybook/react-webpack5@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5): resolution: {integrity: sha512-pnl11MYKM3jRmHQz2dSnEDfDiApdH7ys3zH/FjImsTK6S8etMKlxGnZ58Puxj05qvrBRgpxnQSL+ZazfrEX/6w==} engines: {node: '>=16.0.0'} peerDependencies: @@ -29639,9 +30788,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@storybook/builder-webpack5': 7.4.2(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) - '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) + '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) '@storybook/react': 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@types/node': 16.11.7 react: 17.0.2 @@ -30153,7 +31302,7 @@ packages: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} engines: {node: '>=14'} dependencies: - '@babel/code-frame': 7.22.13 + '@babel/code-frame': 7.24.2 '@babel/runtime': 7.23.2 '@types/aria-query': 5.0.2 aria-query: 5.1.3 @@ -30443,7 +31592,7 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.5.1 + '@types/node': 14.18.42 /@types/bonjour@3.5.10: resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} @@ -30792,7 +31941,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.5.1 + '@types/node': 14.18.42 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 @@ -31210,6 +32359,8 @@ packages: /@types/node@20.5.1: resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} requiresBuild: true + dev: true + optional: true /@types/nodemailer@6.4.11: resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==} @@ -31432,7 +32583,7 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.5.1 + '@types/node': 14.18.42 /@types/shimmer@1.0.5: resolution: {integrity: sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==} @@ -31699,7 +32850,7 @@ packages: typescript: optional: true dependencies: - '@eslint-community/regexpp': 4.5.0 + '@eslint-community/regexpp': 4.9.1 '@typescript-eslint/parser': 5.58.0(eslint@8.51.0)(typescript@4.9.5) '@typescript-eslint/scope-manager': 5.58.0 '@typescript-eslint/type-utils': 5.58.0(eslint@8.51.0)(typescript@4.9.5) @@ -34164,7 +35315,7 @@ packages: /axios@0.21.4: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.5(debug@4.3.4) transitivePeerDependencies: - debug dev: false @@ -34172,7 +35323,7 @@ packages: /axios@0.26.1: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 transitivePeerDependencies: - debug dev: false @@ -34180,7 +35331,7 @@ packages: /axios@0.28.0(debug@4.3.4): resolution: {integrity: sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.5(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -34190,7 +35341,7 @@ packages: /axios@1.1.3: resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.5(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -34200,7 +35351,7 @@ packages: /axios@1.6.0: resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -34210,7 +35361,7 @@ packages: /axios@1.6.2: resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -34219,7 +35370,7 @@ packages: /axios@1.6.7: resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: - follow-redirects: 1.15.5 + follow-redirects: 1.15.5(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -34328,14 +35479,14 @@ packages: webpack: 5.78.0(@swc/core@1.3.49)(esbuild@0.18.20)(webpack-cli@5.1.4) dev: true - /babel-loader@8.3.0(@babel/core@7.23.2)(webpack@5.82.1): + /babel-loader@8.3.0(@babel/core@7.24.4)(webpack@5.82.1): resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} engines: {node: '>= 8.9'} peerDependencies: '@babel/core': ^7.0.0 webpack: '>=2' dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 find-cache-dir: 3.3.2 loader-utils: 2.0.4 make-dir: 3.1.0 @@ -34499,6 +35650,19 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: true + + /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.24.4): + resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.23.2 + '@babel/core': 7.24.4 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.4) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color /babel-plugin-polyfill-corejs3@0.8.5(@babel/core@7.22.11): resolution: {integrity: sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==} @@ -34534,6 +35698,18 @@ packages: core-js-compat: 3.32.2 transitivePeerDependencies: - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.8.5(@babel/core@7.24.4): + resolution: {integrity: sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.4) + core-js-compat: 3.32.2 + transitivePeerDependencies: + - supports-color /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.22.11): resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} @@ -34566,6 +35742,17 @@ packages: '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.2) transitivePeerDependencies: - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.24.4): + resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.4) + transitivePeerDependencies: + - supports-color /babel-plugin-react-docgen@4.2.1: resolution: {integrity: sha512-UQ0NmGHj/HAqi5Bew8WvNfCk8wSsmdgNd8ZdMjBCICtyCJCq9LiqgqvjCYe570/Wg7AQArSq1VQ60Dd/CHN7mQ==} @@ -39261,8 +40448,8 @@ packages: '@babel/plugin-transform-react-jsx': ^7.14.9 eslint: ^8.1.0 dependencies: - '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.23.2) - '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.2) + '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.24.4) eslint: 8.51.0 lodash: 4.17.21 string-natural-compare: 3.0.1 @@ -39425,7 +40612,7 @@ packages: damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 8.51.0 - has: 1.0.3 + has: 1.0.4 jsx-ast-utils: 3.3.3 language-tags: 1.0.5 minimatch: 3.1.2 @@ -40759,7 +41946,7 @@ packages: tslib: 2.6.2 dev: false - /follow-redirects@1.15.2(debug@4.3.4): + /follow-redirects@1.15.2: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} peerDependencies: @@ -40767,10 +41954,8 @@ packages: peerDependenciesMeta: debug: optional: true - dependencies: - debug: 4.3.4(supports-color@8.1.1) - /follow-redirects@1.15.5: + /follow-redirects@1.15.5(debug@4.3.4): resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} peerDependencies: @@ -40778,6 +41963,8 @@ packages: peerDependenciesMeta: debug: optional: true + dependencies: + debug: 4.3.4(supports-color@8.1.1) /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -42566,7 +43753,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -44153,7 +45340,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@16.11.7)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) transitivePeerDependencies: - bufferutil - canvas @@ -54936,11 +56123,11 @@ packages: peerDependencies: rollup: ^2.0.0 dependencies: - '@babel/code-frame': 7.22.13 + '@babel/code-frame': 7.21.4 jest-worker: 26.6.2 rollup: 2.79.1 serialize-javascript: 4.0.0 - terser: 5.22.0 + terser: 5.16.9 dev: true /rollup-plugin-terser@7.0.2(rollup@3.20.2): @@ -57716,7 +58903,7 @@ packages: tslib: 1.14.1 dev: true - /ts-jest@27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5): + /ts-jest@27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} hasBin: true @@ -57737,7 +58924,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@types/jest': 27.5.2 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -57751,7 +58938,7 @@ packages: yargs-parser: 20.2.9 dev: true - /ts-jest@27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5): + /ts-jest@27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} hasBin: true @@ -57772,7 +58959,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@types/jest': 29.5.1 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -57786,7 +58973,7 @@ packages: yargs-parser: 20.2.9 dev: true - /ts-jest@27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5): + /ts-jest@27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} hasBin: true @@ -57807,7 +58994,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@types/jest': 29.5.2 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -57821,7 +59008,7 @@ packages: yargs-parser: 20.2.9 dev: true - /ts-jest@29.1.0(@babel/core@7.23.2)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.1.0(@babel/core@7.24.4)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -57842,7 +59029,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 bs-logger: 0.2.6 esbuild: 0.18.20 fast-json-stable-stringify: 2.1.0 @@ -57856,7 +59043,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.1.0(@babel/core@7.23.2)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.1.0(@babel/core@7.24.4)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -57877,7 +59064,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 jest: 29.5.0(@types/node@14.18.42)(ts-node@10.9.1) @@ -57890,7 +59077,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.1.2(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.5): + /ts-jest@29.1.2(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.5): resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -57911,7 +59098,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 jest: 29.7.0(@types/node@14.18.42)(ts-node@10.9.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8118e6c86c2..ed81fa8c04c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,3 +11,4 @@ packages: # all packages in enterprise modules - 'enterprise/packages/*' - 'enterprise/packages/libs/*' + - 'enterprise/packages/web/*' From c79d0bc1681543815c747641184bca94e3d5338d Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Tue, 9 Apr 2024 18:41:31 +0100 Subject: [PATCH 08/33] chore: update (#5388) --- apps/web/src/pages/auth/QuestionnairePage.tsx | 16 +++++++++++++--- .../src/types/feature-flags/feature-flags.ts | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/auth/QuestionnairePage.tsx b/apps/web/src/pages/auth/QuestionnairePage.tsx index 2afcf1ab85e..04d1c4df411 100644 --- a/apps/web/src/pages/auth/QuestionnairePage.tsx +++ b/apps/web/src/pages/auth/QuestionnairePage.tsx @@ -3,13 +3,18 @@ import AuthContainer from '../../components/layout/components/AuthContainer'; import { QuestionnaireForm } from './components/QuestionnaireForm'; import { useVercelIntegration } from '../../hooks'; import SetupLoader from './components/SetupLoader'; -import { ENV, IS_DOCKER_HOSTED } from '@novu/shared-web'; +import { ENV, IS_DOCKER_HOSTED, useFeatureFlag } from '@novu/shared-web'; import { HubspotSignupForm } from './components/HubspotSignupForm'; +import { FeatureFlagsKeysEnum } from '@novu/shared'; +import { When } from '@novu/design-system'; export default function QuestionnairePage() { const { isLoading } = useVercelIntegration(); + const isHubspotFormEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_HUBSPOT_ONBOARDING_ENABLED); const isNovuProd = !IS_DOCKER_HOSTED && ENV === 'production'; + const shouldUseHubspotForm = isHubspotFormEnabled && isNovuProd; + return ( {isLoading ? ( @@ -17,9 +22,14 @@ export default function QuestionnairePage() { ) : ( - {!isNovuProd ? : } + + + + + + )} diff --git a/libs/shared/src/types/feature-flags/feature-flags.ts b/libs/shared/src/types/feature-flags/feature-flags.ts index 15209ac9611..1eb4cf87b64 100644 --- a/libs/shared/src/types/feature-flags/feature-flags.ts +++ b/libs/shared/src/types/feature-flags/feature-flags.ts @@ -11,4 +11,6 @@ export enum FeatureFlagsKeysEnum { IS_ECHO_ENABLED = 'IS_ECHO_ENABLED', IS_IMPROVED_ONBOARDING_ENABLED = 'IS_IMPROVED_ONBOARDING_ENABLED', IS_NEW_MESSAGES_API_RESPONSE_ENABLED = 'IS_NEW_MESSAGES_API_RESPONSE_ENABLED', + IS_BILLING_REVERSE_TRIAL_ENABLED = 'IS_BILLING_REVERSE_TRIAL_ENABLED', + IS_HUBSPOT_ONBOARDING_ENABLED = 'IS_HUBSPOT_ONBOARDING_ENABLED', } From 43aea87870e967ac145beb4d423c38e823c036f2 Mon Sep 17 00:00:00 2001 From: Biswajeet Das Date: Wed, 3 Apr 2024 22:40:35 +0530 Subject: [PATCH 09/33] fix: cache validation during notification template promotion (#5352) --- ...te-notification-template-change.usecase.ts | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/apps/api/src/app/change/usecases/promote-notification-template-change/promote-notification-template-change.usecase.ts b/apps/api/src/app/change/usecases/promote-notification-template-change/promote-notification-template-change.usecase.ts index 915c45c0f63..1a61fac3610 100644 --- a/apps/api/src/app/change/usecases/promote-notification-template-change/promote-notification-template-change.usecase.ts +++ b/apps/api/src/app/change/usecases/promote-notification-template-change/promote-notification-template-change.usecase.ts @@ -179,19 +179,7 @@ export class PromoteNotificationTemplateChange { return; } - await this.invalidateCache.invalidateByKey({ - key: buildNotificationTemplateKey({ - _id: newItem._id, - _environmentId: command.environmentId, - }), - }); - - await this.invalidateCache.invalidateByKey({ - key: buildNotificationTemplateIdentifierKey({ - templateIdentifier: newItem.triggers[0].identifier, - _environmentId: command.environmentId, - }), - }); + await this.invalidateNotificationTemplate(item, command.organizationId); return await this.notificationTemplateRepository.update( { @@ -215,15 +203,53 @@ export class PromoteNotificationTemplateChange { ); } - private async invalidateBlueprints(command: PromoteTypeChangeCommand) { - if (command.organizationId === this.blueprintOrganizationId) { - await this.invalidateCache.invalidateByKey({ - key: buildGroupedBlueprintsKey(), - }); + private async getProductionEnvironmentId(organizationId: string) { + const productionEnvironmentId = ( + await this.environmentRepository.findOrganizationEnvironments(organizationId) + )?.find((env) => env.name === 'Production')?._id; + + if (!productionEnvironmentId) { + throw new NotFoundException('Production environment not found'); } + + return productionEnvironmentId; } private get blueprintOrganizationId() { return NotificationTemplateRepository.getBlueprintOrganizationId(); } + + private async invalidateBlueprints(command: PromoteTypeChangeCommand) { + if (command.organizationId === this.blueprintOrganizationId) { + const productionEnvironmentId = await this.getProductionEnvironmentId(this.blueprintOrganizationId); + + if (productionEnvironmentId) { + await this.invalidateCache.invalidateByKey({ + key: buildGroupedBlueprintsKey(productionEnvironmentId), + }); + } + } + } + + private async invalidateNotificationTemplate(item: NotificationTemplateEntity, organizationId: string) { + const productionEnvironmentId = await this.getProductionEnvironmentId(organizationId); + + /** + * Only invalidate cache of Production environment cause the development environment cache invalidation is handled + * during the CRUD operations itself + */ + await this.invalidateCache.invalidateByKey({ + key: buildNotificationTemplateKey({ + _id: item._id, + _environmentId: productionEnvironmentId, + }), + }); + + await this.invalidateCache.invalidateByKey({ + key: buildNotificationTemplateIdentifierKey({ + templateIdentifier: item.triggers[0].identifier, + _environmentId: productionEnvironmentId, + }), + }); + } } From f7cbef4e69af8fe00b2beb16c0149da6e4d112c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Tymczuk?= Date: Wed, 10 Apr 2024 09:46:45 +0200 Subject: [PATCH 10/33] Merge pull request #5389 from novuhq/feature/resend-reply-to feat: add reply to field in resend --- providers/resend/src/lib/resend.provider.spec.ts | 3 +++ providers/resend/src/lib/resend.provider.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/providers/resend/src/lib/resend.provider.spec.ts b/providers/resend/src/lib/resend.provider.spec.ts index 31cde396fd3..e3a492e8516 100644 --- a/providers/resend/src/lib/resend.provider.spec.ts +++ b/providers/resend/src/lib/resend.provider.spec.ts @@ -13,6 +13,7 @@ const mockNovuMessage = { to: ['test@test.com'], html: '
Mail Content
', subject: 'Test subject', + reply_to: 'no-reply@novu.co', attachments: [ { mime: 'text/plain', @@ -40,6 +41,7 @@ test('should trigger resend library correctly', async () => { html: mockNovuMessage.html, subject: mockNovuMessage.subject, attachments: mockNovuMessage.attachments, + reply_to: mockNovuMessage.reply_to, }); }); @@ -69,6 +71,7 @@ test('should trigger resend email with From Name', async () => { filename: attachment?.name, content: attachment.file, })), + reply_to: null, cc: undefined, bcc: undefined, }); diff --git a/providers/resend/src/lib/resend.provider.ts b/providers/resend/src/lib/resend.provider.ts index ec924a64c30..cf8061ab72b 100644 --- a/providers/resend/src/lib/resend.provider.ts +++ b/providers/resend/src/lib/resend.provider.ts @@ -36,6 +36,7 @@ export class ResendEmailProvider implements IEmailProvider { text: options.text, html: options.html, cc: options.cc, + reply_to: options.replyTo || null, attachments: options.attachments?.map((attachment) => ({ filename: attachment?.name, content: attachment.file, From 3300e22ea733a07c16c0e64f8ee7ee2b6fb7d49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Wed, 10 Apr 2024 10:37:23 +0200 Subject: [PATCH 11/33] chore(api): added the missing code after the cherry-pick --- .../src/app/blueprint/blueprint.controller.ts | 31 ++++++++++++++++--- .../e2e/get-grouped-blueprints.e2e.ts | 4 ++- .../get-grouped-blueprints.command.ts | 4 +-- .../get-grouped-blueprints.usecase.ts | 6 ++-- ...te-notification-template-change.usecase.ts | 2 ++ .../services/cache/key-builders/entities.ts | 4 +-- 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/apps/api/src/app/blueprint/blueprint.controller.ts b/apps/api/src/app/blueprint/blueprint.controller.ts index b9b17deb748..0efb9b61dcb 100644 --- a/apps/api/src/app/blueprint/blueprint.controller.ts +++ b/apps/api/src/app/blueprint/blueprint.controller.ts @@ -1,8 +1,9 @@ import { ClassSerializerInterceptor, Controller, Get, Param, UseInterceptors } from '@nestjs/common'; +import { EnvironmentRepository, NotificationTemplateRepository } from '@novu/dal'; import { GroupedBlueprintResponse } from './dto/grouped-blueprint.response.dto'; import { GetBlueprint, GetBlueprintCommand } from './usecases/get-blueprint'; -import { GetGroupedBlueprints } from './usecases/get-grouped-blueprints'; +import { GetGroupedBlueprints, GetGroupedBlueprintsCommand } from './usecases/get-grouped-blueprints'; import { GetBlueprintResponse } from './dto/get-blueprint.response.dto'; import { ApiCommonResponses } from '../shared/framework/response.decorator'; @@ -10,11 +11,33 @@ import { ApiCommonResponses } from '../shared/framework/response.decorator'; @Controller('/blueprints') @UseInterceptors(ClassSerializerInterceptor) export class BlueprintController { - constructor(private getBlueprintUsecase: GetBlueprint, private getGroupedBlueprintsUsecase: GetGroupedBlueprints) {} + constructor( + private environmentRepository: EnvironmentRepository, + private getBlueprintUsecase: GetBlueprint, + private getGroupedBlueprintsUsecase: GetGroupedBlueprints + ) {} @Get('/group-by-category') - getGroupedBlueprints(): Promise { - return this.getGroupedBlueprintsUsecase.execute(); + async getGroupedBlueprints(): Promise { + const prodEnvironmentId = await this.getProdEnvironmentId(); + + return this.getGroupedBlueprintsUsecase.execute( + GetGroupedBlueprintsCommand.create({ environmentId: prodEnvironmentId }) + ); + } + + private async getProdEnvironmentId() { + const productionEnvironmentId = ( + await this.environmentRepository.findOrganizationEnvironments( + NotificationTemplateRepository.getBlueprintOrganizationId() || '' + ) + )?.find((env) => env.name === 'Production')?._id; + + if (!productionEnvironmentId) { + throw new Error('Production environment id was not found'); + } + + return productionEnvironmentId; } @Get('/:templateIdOrIdentifier') diff --git a/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts b/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts index c192b7850a1..252ccfaa04b 100644 --- a/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts +++ b/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts @@ -112,6 +112,8 @@ describe('Get grouped notification template blueprints - /blueprints/group-by-ca it('should update the static POPULAR_TEMPLATES_GROUPED with fresh data', async () => { const prodEnv = await getProductionEnvironment(); + if (!prodEnv) throw new Error('production environment was not found'); + await createTemplateFromBlueprint({ session, notificationTemplateRepository, prodEnv }); const data = await session.testAgent.get(`/v1/blueprints/group-by-category`).send(); @@ -128,7 +130,7 @@ describe('Get grouped notification template blueprints - /blueprints/group-by-ca indexModuleStub.value(mockedValue); await invalidateCache.invalidateByKey({ - key: buildGroupedBlueprintsKey(), + key: buildGroupedBlueprintsKey(prodEnv._id), }); const updatedBlueprintFromDb = (await session.testAgent.get(`/v1/blueprints/group-by-category`).send()).body.data diff --git a/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.command.ts b/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.command.ts index 75655721246..f72675eeda9 100644 --- a/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.command.ts +++ b/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.command.ts @@ -1,3 +1,3 @@ -import { BaseCommand } from '@novu/application-generic'; +import { EnvironmentLevelCommand } from '@novu/application-generic'; -export class GetGroupedBlueprintsCommand extends BaseCommand {} +export class GetGroupedBlueprintsCommand extends EnvironmentLevelCommand {} diff --git a/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.usecase.ts b/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.usecase.ts index 613c3259038..95bb8599bde 100644 --- a/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.usecase.ts +++ b/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.usecase.ts @@ -4,7 +4,7 @@ import { buildGroupedBlueprintsKey, CachedEntity } from '@novu/application-gener import { INotificationTemplate, IGroupedBlueprint } from '@novu/shared'; import { GroupedBlueprintResponse } from '../../dto/grouped-blueprint.response.dto'; -import { POPULAR_GROUPED_NAME, POPULAR_TEMPLATES_ID_LIST } from './index'; +import { GetGroupedBlueprintsCommand, POPULAR_GROUPED_NAME, POPULAR_TEMPLATES_ID_LIST } from './index'; const WEEK_IN_SECONDS = 60 * 60 * 24 * 7; @@ -13,10 +13,10 @@ export class GetGroupedBlueprints { constructor(private notificationTemplateRepository: NotificationTemplateRepository) {} @CachedEntity({ - builder: () => buildGroupedBlueprintsKey(), + builder: (command: GetGroupedBlueprintsCommand) => buildGroupedBlueprintsKey(command.environmentId), options: { ttl: WEEK_IN_SECONDS }, }) - async execute(): Promise { + async execute(command: GetGroupedBlueprintsCommand): Promise { const groups = await this.fetchGroupedBlueprints(); const updatePopularBlueprints = this.updatePopularBlueprints(groups); diff --git a/apps/api/src/app/change/usecases/promote-notification-template-change/promote-notification-template-change.usecase.ts b/apps/api/src/app/change/usecases/promote-notification-template-change/promote-notification-template-change.usecase.ts index 1a61fac3610..8db69be4a37 100644 --- a/apps/api/src/app/change/usecases/promote-notification-template-change/promote-notification-template-change.usecase.ts +++ b/apps/api/src/app/change/usecases/promote-notification-template-change/promote-notification-template-change.usecase.ts @@ -7,6 +7,7 @@ import { NotificationStepEntity, NotificationGroupRepository, StepVariantEntity, + EnvironmentRepository, } from '@novu/dal'; import { ChangeEntityTypeEnum } from '@novu/shared'; import { @@ -24,6 +25,7 @@ export class PromoteNotificationTemplateChange { constructor( private invalidateCache: InvalidateCacheService, private notificationTemplateRepository: NotificationTemplateRepository, + private environmentRepository: EnvironmentRepository, private messageTemplateRepository: MessageTemplateRepository, private notificationGroupRepository: NotificationGroupRepository, @Inject(forwardRef(() => ApplyChange)) private applyChange: ApplyChange, diff --git a/packages/application-generic/src/services/cache/key-builders/entities.ts b/packages/application-generic/src/services/cache/key-builders/entities.ts index b482a3cbc10..5c45baa4af1 100644 --- a/packages/application-generic/src/services/cache/key-builders/entities.ts +++ b/packages/application-generic/src/services/cache/key-builders/entities.ts @@ -84,12 +84,12 @@ const buildEnvironmentByApiKey = ({ apiKey }: { apiKey: string }): string => identifierPrefix: IdentifierPrefixEnum.API_KEY, }); -const buildGroupedBlueprintsKey = (): string => +const buildGroupedBlueprintsKey = (environmentId: string): string => buildCommonKey({ type: CacheKeyTypeEnum.ENTITY, keyEntity: CacheKeyPrefixEnum.GROUPED_BLUEPRINTS, environmentIdPrefix: OrgScopePrefixEnum.ORGANIZATION_ID, - environmentId: process.env.BLUEPRINT_CREATOR, + environmentId: environmentId, identifierPrefix: IdentifierPrefixEnum.GROUPED_BLUEPRINT, identifier: BLUEPRINT_IDENTIFIER, }); From 900e98f4bdc1bf51c8c7c758e05479d278b6b78d Mon Sep 17 00:00:00 2001 From: Gosha Date: Thu, 11 Apr 2024 11:44:14 +0300 Subject: [PATCH 12/33] refactor(echo-web): dependency --- enterprise/packages/web/echo/package.json | 4 ++-- pnpm-lock.yaml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/enterprise/packages/web/echo/package.json b/enterprise/packages/web/echo/package.json index f91e2c83f97..4262b7a49de 100644 --- a/enterprise/packages/web/echo/package.json +++ b/enterprise/packages/web/echo/package.json @@ -31,12 +31,12 @@ "@rjsf/core": "^5.17.1", "@rjsf/validator-ajv8": "^5.17.1", "@tanstack/react-query": "^4.20.4", - "eslint": "^8.33.0", - "eslint-plugin-react-hooks": "^4.4.0", "react-router-dom": "6.2.2", "tslib": "^2.3.1" }, "devDependencies": { + "eslint": "^8.33.0", + "eslint-plugin-react-hooks": "^4.4.0", "@types/node": "^18.11.12", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9e8f1dced6..831e012e299 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2367,12 +2367,6 @@ importers: '@tanstack/react-query': specifier: ^4.20.4 version: 4.29.1(react-dom@17.0.2)(react@17.0.2) - eslint: - specifier: ^8.33.0 - version: 8.51.0 - eslint-plugin-react-hooks: - specifier: ^4.4.0 - version: 4.6.0(eslint@8.51.0) react-router-dom: specifier: 6.2.2 version: 6.2.2(react-dom@17.0.2)(react@17.0.2) @@ -2389,6 +2383,12 @@ importers: '@types/react-dom': specifier: ^17.0.0 version: 17.0.20 + eslint: + specifier: ^8.33.0 + version: 8.51.0 + eslint-plugin-react-hooks: + specifier: ^4.4.0 + version: 4.6.0(eslint@8.51.0) react: specifier: ^17.0.1 version: 17.0.2 From 43c7fd05ea3f42ea5f942a1e9cb133b9f9e19bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Fri, 12 Apr 2024 00:30:50 +0200 Subject: [PATCH 13/33] chore(web): migrated all the main-functionality cypress tests to playwright --- apps/web/tests/main-functionality.spec.ts | 346 +++++++++++++++++++++- 1 file changed, 343 insertions(+), 3 deletions(-) diff --git a/apps/web/tests/main-functionality.spec.ts b/apps/web/tests/main-functionality.spec.ts index c0c4fbc7d6e..0ccbf787aa6 100644 --- a/apps/web/tests/main-functionality.spec.ts +++ b/apps/web/tests/main-functionality.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test'; import os from 'node:os'; -import { getByTestId, initializeSession } from './utils.ts/browser'; +import { dragAndDrop, getByTestId, initializeSession } from './utils.ts/browser'; import { addAndEditChannel, editChannel, @@ -119,8 +119,8 @@ test('should save avatar enabled and content for in app', async ({ page }) => { await updateWorkflowButtonClick(page); - expect(getByTestId(page, 'enabled-avatar')).toBeChecked(); - expect(getByTestId(page, 'avatar-icon-info')).toBeVisible(); + await expect(getByTestId(page, 'enabled-avatar')).toBeChecked(); + await expect(getByTestId(page, 'avatar-icon-info')).toBeVisible(); }); test('should edit in-app notification', async ({ page }) => { @@ -235,3 +235,343 @@ test('should update notification active status', async ({ page }) => { await expect(page.getByText('Inactive')).toBeVisible(); }); + +test('should toggle active states of channels', async ({ page }) => { + await page.goto(`/workflows/create`); + + await fillBasicNotificationDetails(page, { title: 'Test toggle active states of channels' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + + let stepActiveSwitch = getByTestId(page, 'step-active-switch'); + await expect(stepActiveSwitch).toHaveValue('on'); + + await stepActiveSwitch.locator('~ label').click({ force: true }); + await stepActiveSwitch.locator('~ label').click({ force: true }); + + await goBack(page); + + await addAndEditChannel(page, 'inApp'); + stepActiveSwitch = getByTestId(page, 'step-active-switch'); + await expect(stepActiveSwitch).toHaveValue('on'); +}); + +test('should show trigger snippet block when editing', async ({ page }) => { + const template = session.templates[0]; + await page.goto(`/workflows/edit/${template._id}`); + + const getSnippetButton = getByTestId(page, 'get-snippet-btn'); + await getSnippetButton.click(); + + const triggerCodeSnippet = getByTestId(page, 'trigger-code-snippet'); + await expect(triggerCodeSnippet).toContainText('test-event'); +}); + +test('should show error on node if message field is missing', async ({ page }) => { + await page.goto(`/workflows/create`); + + await fillBasicNotificationDetails(page); + await goBack(page); + + await dragAndDrop(page, `dnd-emailSelector`, 'addNodeButton'); + let emailNode = getByTestId(page, 'node-emailSelector'); + let errorCircle = getByTestId(emailNode, 'error-circle'); + await expect(errorCircle).toBeVisible(); + + await editChannel(page, 'email'); + const emailSubject = getByTestId(page, 'emailSubject'); + await expect(emailSubject).toHaveClass(/mantine-TextInput-invalid/); + + await emailSubject.fill('this is email subject'); + await goBack(page); + emailNode = getByTestId(page, 'node-emailSelector'); + errorCircle = getByTestId(emailNode, 'error-circle'); + await expect(errorCircle).not.toBeVisible(); +}); + +test('should allow uploading a logo from email editor', async ({ page }) => { + await page.route('**/organizations', async (route) => { + const response = await page.request.fetch(route.request()); + const body = await response.json(); + if (body) { + delete body.data[0].branding.logo; + } + + await route.fulfill({ + response, + body, + }); + }); + + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Test allow uploading a logo from email editor' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + const uploadImageButton = getByTestId(page, 'upload-image-button'); + await uploadImageButton.click(); + + const modalButton = page.getByRole('button', { name: 'Yes' }); + + await modalButton.click(); + await expect(page.url()).toContain('/brand'); +}); + +test('should show the brand logo on main page', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Test allow uploading a logo from email editor' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + + const brandLogo = getByTestId(page, 'brand-logo'); + await expect(brandLogo).toHaveAttribute('src', 'https://web.novu.co/static/images/logo-light.png'); +}); + +test('should support RTL text content', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Test support RTL text content' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + + let editableTextContent = getByTestId(page, 'editable-text-content'); + await editableTextContent.hover(); + await expect(editableTextContent).toHaveCSS('text-align', 'left'); + + const settingsRowButton = getByTestId(page, 'settings-row-btn'); + await settingsRowButton.click(); + + const alignRightButton = getByTestId(page, 'align-right-btn'); + await alignRightButton.click(); + editableTextContent = getByTestId(page, 'editable-text-content'); + await expect(editableTextContent).toHaveCSS('text-align', 'right'); +}); + +test('should create an SMS channel message', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Test SMS Notification Title' }); + await goBack(page); + + await addAndEditChannel(page, 'sms'); + + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('{{firstName}} someone assigned you to {{taskName}}'); + await goBack(page); + + const submitButton = getByTestId(page, 'notification-template-submit-btn'); + await submitButton.click(); + + const getSnippetButton = getByTestId(page, 'get-snippet-btn'); + await getSnippetButton.click(); + const workflowSidebar = getByTestId(page, 'workflow-sidebar'); + await expect(workflowSidebar).toBeVisible(); + const triggerCodeSnippet = getByTestId(workflowSidebar, 'trigger-code-snippet'); + await expect(triggerCodeSnippet).toContainText('test-sms-notification-title'); + await expect(triggerCodeSnippet).toContainText("import { Novu } from '@novu/node'"); + await expect(triggerCodeSnippet).toContainText('taskName'); + await expect(triggerCodeSnippet).toContainText('firstName'); +}); + +test('should save HTML template email', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Custom Code HTML Notification Title' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + + const subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.fill('this is email subject'); + + await page + .locator('[data-test-id="editor-type-selector"] .mantine-Tabs-tabsList') + .getByText(/Custom Code/) + .first() + .click(); + + let editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('Hello world code {{name}}
Test
'); + + await goBack(page); + + await editChannel(page, 'email'); + + editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await expect(editorParent).toContainText('Hello world code {{name}}
Test
'); +}); + +test('should redirect to the workflows page when switching environments', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Environment Switching' }); + await goBack(page); + + await updateWorkflowButtonClick(page); + + await page.goto(`/changes`); + const promoteChangesPromise = page.waitForResponse((response) => { + return !!response.url().match(/\/v1\/changes\/.*\/apply/) && response.request().method() === 'POST'; + }); + const promoteButton = getByTestId(page, 'promote-btn').first(); + await promoteButton.click(); + await promoteChangesPromise; + + let environmentSwitchPromise = page.waitForResponse((response) => { + return !!response.url().match(/\/auth\/environments\/.*\/switch/) && response.request().method() === 'POST'; + }); + let environmentSwitch = getByTestId(page, 'environment-switch'); + const productionButton = environmentSwitch.getByText('Production'); + await productionButton.click(); + await environmentSwitchPromise; + await expect(page).toHaveURL(/\/workflows/); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(/Environment Switching/).click(); + await expect(page).toHaveURL(/\/workflows\/edit/); + + environmentSwitchPromise = page.waitForResponse((response) => { + return !!response.url().match(/\/auth\/environments\/.*\/switch/) && response.request().method() === 'POST'; + }); + environmentSwitch = getByTestId(page, 'environment-switch'); + const developmentButton = environmentSwitch.getByText('Development'); + await developmentButton.click(); + await environmentSwitchPromise; + await expect(page).toHaveURL(/\/workflows/); +}); + +test('New workflow button should be disabled in the Production', async ({ page }) => { + await page.goto(`/workflows`); + + let environmentSwitch = getByTestId(page, 'environment-switch'); + const productionButton = environmentSwitch.getByText('Production'); + await productionButton.click(); + + const createWorkflowButton = getByTestId(page, 'create-workflow-btn'); + await expect(createWorkflowButton).toBeDisabled(); +}); + +test('Should not allow to go to New Template page in Production', async ({ page }) => { + await page.goto(`/workflows/create`); + + let environmentSwitch = getByTestId(page, 'environment-switch'); + const productionButton = environmentSwitch.getByText('Production'); + await productionButton.click(); + + await expect(page.url()).toContain('/workflows'); +}); + +test('should save Cta buttons state in inApp channel', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'In App CTA Button' }); + await goBack(page); + + await addAndEditChannel(page, 'inApp'); + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('Text content'); + + const controlAdd = getByTestId(page, 'control-add').first(); + await controlAdd.click(); + + const clickArea = getByTestId(page, 'template-container-click-area').first(); + await clickArea.click(); + + await goBack(page); + + await updateWorkflowButtonClick(page); + + await page.goto(`/workflows`); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(/In App CTA Button/).click(); + await expect(page.url()).toContain('/workflows/edit'); + + await editChannel(page, 'inApp'); + + const templateContainerInput = getByTestId(page, 'template-container').first().locator('input'); + await expect(templateContainerInput).toHaveCount(1); + + const removeButton = getByTestId(page, 'remove-button-icon'); + await removeButton.click(); + + await goBack(page); + + await editChannel(page, 'inApp'); + getByTestId(page, 'control-add').first(); +}); + +test('should load successfully the recently created notification template, when going back from editor -> templates list -> editor', async ({ + page, +}) => { + await page.goto(`/workflows`); + + const createWorkflowButton = getByTestId(page, 'create-workflow-btn'); + await createWorkflowButton.click(); + + const createBlankWorkflow = getByTestId(page, 'create-workflow-blank'); + await createBlankWorkflow.click(); + + await fillBasicNotificationDetails(page, { title: 'Test notification' }); + await goBack(page); + + await addAndEditChannel(page, 'inApp'); + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('Test in-app'); + await goBack(page); + + await addAndEditChannel(page, 'email'); + const editableText = getByTestId(page, 'editable-text-content'); + await editableText.clear(); + await editableText.pressSequentially('Test email'); + const subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.fill('this is email subject'); + const emailPreheader = getByTestId(page, 'emailPreheader'); + await emailPreheader.fill('this is email preheader'); + await goBack(page); + + await updateWorkflowButtonClick(page); + + const workflowsLink = getByTestId(page, 'side-nav-templates-link'); + await workflowsLink.click(); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(/Test notification/).click(); + await expect(page.url()).toContain('/workflows/edit'); + + const inAppNode = getByTestId(page, 'node-inAppSelector'); + await expect(inAppNode).toBeVisible(); + const emailNode = getByTestId(page, 'node-emailSelector'); + await expect(emailNode).toBeVisible(); +}); + +test('should load successfully the same notification template, when going back from templates list -> editor -> templates list -> editor', async ({ + page, +}) => { + await page.goto(`/workflows`); + const template = session.templates[0]; + + let notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(template.name).click(); + await expect(page.url()).toContain('/workflows/edit'); + + let inAppNode = getByTestId(page, 'node-inAppSelector'); + await expect(inAppNode).toBeVisible(); + let emailNode = getByTestId(page, 'node-emailSelector'); + await expect(emailNode).toBeVisible(); + + const workflowsLink = getByTestId(page, 'side-nav-templates-link'); + await workflowsLink.click(); + + notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(template.name).click(); + await expect(page.url()).toContain('/workflows/edit'); + + inAppNode = getByTestId(page, 'node-inAppSelector'); + await expect(inAppNode).toBeVisible(); + emailNode = getByTestId(page, 'node-emailSelector'); + await expect(emailNode).toBeVisible(); +}); From cb51655673cf2c5d91b67779590aa41181d460e8 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Fri, 12 Apr 2024 17:17:34 +0300 Subject: [PATCH 14/33] feat(auth-v2): Add externalId to user and organizations Introduce externalId field to user and organization collections to store the unique id of the external user management provider. --- libs/dal/src/repositories/organization/organization.entity.ts | 2 ++ libs/dal/src/repositories/organization/organization.schema.ts | 1 + libs/dal/src/repositories/user/user.entity.ts | 2 ++ libs/dal/src/repositories/user/user.schema.ts | 1 + libs/shared/src/entities/organization/organization.interface.ts | 1 + libs/shared/src/entities/user/user.interface.ts | 1 + 6 files changed, 8 insertions(+) diff --git a/libs/dal/src/repositories/organization/organization.entity.ts b/libs/dal/src/repositories/organization/organization.entity.ts index 2d5790fcec4..23669ad663d 100644 --- a/libs/dal/src/repositories/organization/organization.entity.ts +++ b/libs/dal/src/repositories/organization/organization.entity.ts @@ -30,6 +30,8 @@ export class OrganizationEntity implements IOrganizationEntity { createdAt: string; updatedAt: string; + + externalId?: string; } export type OrganizationDBModel = OrganizationEntity; diff --git a/libs/dal/src/repositories/organization/organization.schema.ts b/libs/dal/src/repositories/organization/organization.schema.ts index 86113eb7936..90dbdac2ad3 100644 --- a/libs/dal/src/repositories/organization/organization.schema.ts +++ b/libs/dal/src/repositories/organization/organization.schema.ts @@ -60,6 +60,7 @@ const organizationSchema = new Schema( default: false, }, }, + externalId: Schema.Types.String, }, schemaOptions ); diff --git a/libs/dal/src/repositories/user/user.entity.ts b/libs/dal/src/repositories/user/user.entity.ts index 3cf131ac94c..d1dce167702 100644 --- a/libs/dal/src/repositories/user/user.entity.ts +++ b/libs/dal/src/repositories/user/user.entity.ts @@ -53,6 +53,8 @@ export class UserEntity implements IUserEntity { servicesHashes?: { intercom?: string }; jobTitle?: JobTitleEnum; + + externalId?: string; } export type UserDBModel = UserEntity; diff --git a/libs/dal/src/repositories/user/user.schema.ts b/libs/dal/src/repositories/user/user.schema.ts index 26541b276c1..8d55b3909cc 100644 --- a/libs/dal/src/repositories/user/user.schema.ts +++ b/libs/dal/src/repositories/user/user.schema.ts @@ -41,6 +41,7 @@ const userSchema = new Schema( intercom: Schema.Types.String, }, jobTitle: Schema.Types.String, + externalId: Schema.Types.String, }, schemaOptions ); diff --git a/libs/shared/src/entities/organization/organization.interface.ts b/libs/shared/src/entities/organization/organization.interface.ts index d25d89baf19..f46173c6444 100644 --- a/libs/shared/src/entities/organization/organization.interface.ts +++ b/libs/shared/src/entities/organization/organization.interface.ts @@ -18,4 +18,5 @@ export interface IOrganizationEntity { productUseCases?: ProductUseCases; createdAt: string; updatedAt: string; + externalId?: string; } diff --git a/libs/shared/src/entities/user/user.interface.ts b/libs/shared/src/entities/user/user.interface.ts index 204f7c111ca..e102d331dc6 100644 --- a/libs/shared/src/entities/user/user.interface.ts +++ b/libs/shared/src/entities/user/user.interface.ts @@ -14,4 +14,5 @@ export interface IUserEntity { showOnBoardingTour?: number; servicesHashes?: IServicesHashes; jobTitle?: JobTitleEnum; + externalId?: string; } From fe17bd4325bad1efa79ae6d247a245fb4545cca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Fri, 12 Apr 2024 17:00:22 +0200 Subject: [PATCH 15/33] feat(web): run playwright tests in the pipeline; migrate a few more test files --- .github/workflows/reusable-web-e2e.yml | 19 +- .../cypress/tests/digest-playground.spec.ts | 6 +- .../tests/integrations-list-modal.spec.ts | 6 +- .../tests/integrations-list-page.spec.ts | 4 + .../main-functionality.spec.ts | 6 +- apps/web/tests/digest-playground.spec.ts | 185 +++ .../web/tests/integrations-list-modal.spec.ts | 843 +++++++++++++ apps/web/tests/integrations-list-page.spec.ts | 1116 +++++++++++++++++ apps/web/tests/utils.ts/browser.ts | 15 +- apps/web/tests/utils.ts/integrations.ts | 88 ++ apps/web/tests/utils.ts/plugins.ts | 38 +- 11 files changed, 2309 insertions(+), 17 deletions(-) create mode 100644 apps/web/tests/digest-playground.spec.ts create mode 100644 apps/web/tests/integrations-list-modal.spec.ts create mode 100644 apps/web/tests/integrations-list-page.spec.ts create mode 100644 apps/web/tests/utils.ts/integrations.ts diff --git a/.github/workflows/reusable-web-e2e.yml b/.github/workflows/reusable-web-e2e.yml index 97c89a0a630..c5c7bdae22e 100644 --- a/.github/workflows/reusable-web-e2e.yml +++ b/.github/workflows/reusable-web-e2e.yml @@ -141,11 +141,26 @@ jobs: config-file: cypress.config.ts spec: "cypress/tests/**/*.spec.{js,jsx,ts,tsx}" - - uses: actions/upload-artifact@v3 - if: failure() + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + working-directory: apps/web + run: pnpm playwright:test + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: apps/web/playwright-report/ + retention-days: 30 + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} with: name: cypress-screenshots path: apps/web/cypress/screenshots + retention-days: 30 component_web: if: "!contains(github.event.head_commit.message, 'ci skip')" diff --git a/apps/web/cypress/tests/digest-playground.spec.ts b/apps/web/cypress/tests/digest-playground.spec.ts index 88fae2c7852..5ffff1d51bd 100644 --- a/apps/web/cypress/tests/digest-playground.spec.ts +++ b/apps/web/cypress/tests/digest-playground.spec.ts @@ -1,4 +1,8 @@ -describe('Digest Playground Workflow Page', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/digest-playground.spec.ts. + * @deprecated + */ +describe.skip('Digest Playground Workflow Page', function () { beforeEach(function () { cy.initializeSession({ noTemplates: true }).as('session'); }); diff --git a/apps/web/cypress/tests/integrations-list-modal.spec.ts b/apps/web/cypress/tests/integrations-list-modal.spec.ts index ccd963585a3..fe8c2b4e0d5 100644 --- a/apps/web/cypress/tests/integrations-list-modal.spec.ts +++ b/apps/web/cypress/tests/integrations-list-modal.spec.ts @@ -18,7 +18,11 @@ Cypress.on('window:before:load', (win) => { win.isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; }); -describe('Integrations List Modal', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/integrations-list-modal.spec.ts. + * @deprecated + */ +describe.skip('Integrations List Modal', function () { let session: any; beforeEach(function () { diff --git a/apps/web/cypress/tests/integrations-list-page.spec.ts b/apps/web/cypress/tests/integrations-list-page.spec.ts index f3b0d339912..7e5c8e7963b 100644 --- a/apps/web/cypress/tests/integrations-list-page.spec.ts +++ b/apps/web/cypress/tests/integrations-list-page.spec.ts @@ -18,6 +18,10 @@ Cypress.on('window:before:load', (win) => { win.isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; }); +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/integrations-list-page.spec.ts. + * @deprecated + */ describe('Integrations List Page', function () { let session: any; diff --git a/apps/web/cypress/tests/notification-editor/main-functionality.spec.ts b/apps/web/cypress/tests/notification-editor/main-functionality.spec.ts index 4a6856c83fa..12a5af166fd 100644 --- a/apps/web/cypress/tests/notification-editor/main-functionality.spec.ts +++ b/apps/web/cypress/tests/notification-editor/main-functionality.spec.ts @@ -1,6 +1,10 @@ import { addAndEditChannel, clickWorkflow, dragAndDrop, editChannel, fillBasicNotificationDetails, goBack } from '.'; -describe('Workflow Editor - Main Functionality', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/main-functionality.spec.ts. + * @deprecated + */ +describe.skip('Workflow Editor - Main Functionality', function () { beforeEach(function () { cy.initializeSession().as('session'); }); diff --git a/apps/web/tests/digest-playground.spec.ts b/apps/web/tests/digest-playground.spec.ts new file mode 100644 index 00000000000..3dce3206873 --- /dev/null +++ b/apps/web/tests/digest-playground.spec.ts @@ -0,0 +1,185 @@ +import { test, expect } from '@playwright/test'; + +import { getByTestId, initializeSession } from './utils.ts/browser'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context, { noTemplates: true }); +}); + +test('should have a link to the docs', async ({ page }) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + const learnMoreLink = page.locator('a[href^="https://docs.novu.co/workflows/digest"]'); + await expect(learnMoreLink).toHaveText('Learn more in docs'); +}); + +test('the set up digest workflow should redirect to template edit page', async ({ page }) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + const setupDigestWorkflowButton = page.getByRole('button', { name: 'Set Up Digest Workflow' }); + await setupDigestWorkflowButton.click(); + + await expect(page).toHaveURL(/\/workflows\/edit/); +}); + +test('should show the digest workflow hints', async ({ page }) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + // click try digest playground + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + // click set up digest workflow + const setupDigestWorkflowButton = page.getByRole('button', { name: 'Set Up Digest Workflow' }); + await setupDigestWorkflowButton.click(); + + // in the template workflow editor + await expect(page).toHaveURL(/\/workflows\/edit/); + + // check the digest hint + let digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).toContainText('Set-up time interval'); + await expect(digestWorkflowTooltip).toContainText( + 'Specify for how long the digest should collect events before sending a digested event to the next step step in the workflow.' + ); + let primaryButton = page.getByRole('button', { name: 'Next' }); + await expect(primaryButton).toBeVisible(); + let skipTourButton = page.getByRole('button', { name: 'Skip tour' }); + await expect(skipTourButton).toBeVisible(); + let dotsNavigation = getByTestId(page, 'digest-workflow-tooltip-dots-navigation'); + await expect(dotsNavigation).toBeVisible(); + + // check if has digest step + const digestNode = getByTestId(page, 'node-digestSelector'); + await expect(digestNode).toBeVisible(); + // check if digest step settings opened + let digestSettings = getByTestId(page, 'step-editor-sidebar'); + await expect(digestSettings).toBeVisible(); + await expect(digestSettings).toContainText('All events'); + + // click next on hint + await primaryButton.click(); + + // check the email hint + digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).toContainText('Set-up email content'); + await expect(digestWorkflowTooltip).toContainText( + 'Use custom HTML or our visual editor to define how the email will look like when sent to the subscriber.' + ); + primaryButton = page.getByRole('button', { name: 'Next' }); + await expect(primaryButton).toBeVisible(); + skipTourButton = page.getByRole('button', { name: 'Skip tour' }); + await expect(skipTourButton).toBeVisible(); + dotsNavigation = getByTestId(page, 'digest-workflow-tooltip-dots-navigation'); + await expect(dotsNavigation).toBeVisible(); + + // check if email step settings opened + digestSettings = getByTestId(page, 'step-editor-sidebar'); + await expect(digestSettings).toBeVisible(); + await expect(digestSettings).toContainText('Email'); + + // click next on hint + await primaryButton.click(); + + // check the email hint + digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).toContainText('Test your workflow'); + await expect(digestWorkflowTooltip).toContainText( + 'We will trigger the workflow multiple times to represent how it aggregates notifications.' + ); + primaryButton = page.getByRole('button', { name: 'Got it' }); + await expect(primaryButton).toBeVisible(); + skipTourButton = page.getByRole('button', { name: 'Skip tour' }); + await expect(skipTourButton).not.toBeVisible(); + dotsNavigation = getByTestId(page, 'digest-workflow-tooltip-dots-navigation'); + await expect(dotsNavigation).toBeVisible(); + + // the step settings should be hidden + const workflowSidebar = getByTestId(page, 'workflow-sidebar'); + await expect(workflowSidebar).toBeVisible(); + await expect(workflowSidebar).toContainText('Trigger'); + + // click got it should hide the hint + await primaryButton.click(); + digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).not.toBeVisible(); +}); + +test('should hide the digest workflow hints when clicking on skip tour button', async ({ page }) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + // click try digest playground + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + // click set up digest workflow + const setupDigestWorkflowButton = page.getByRole('button', { name: 'Set Up Digest Workflow' }); + await setupDigestWorkflowButton.click(); + + // in the template workflow editor + await expect(page).toHaveURL(/\/workflows\/edit/); + + // check the digest hint + let digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).toContainText('Set-up time interval'); + const skipTourButton = page.getByRole('button', { name: 'Skip tour' }); + await skipTourButton.click(); + + digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).not.toBeVisible(); +}); + +test('when clicking on the back button from the playground it should redirect to /get-started/preview', async ({ + page, +}) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + // click try digest playground + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + // click set up digest workflow + const goBack = page.getByRole('button', { name: 'Go Back' }); + await goBack.click(); + + // in the template workflow editor + await expect(page).toHaveURL(/\/get-started\/preview/); +}); diff --git a/apps/web/tests/integrations-list-modal.spec.ts b/apps/web/tests/integrations-list-modal.spec.ts new file mode 100644 index 00000000000..fd889d85bd7 --- /dev/null +++ b/apps/web/tests/integrations-list-modal.spec.ts @@ -0,0 +1,843 @@ +import { + ChannelTypeEnum, + chatProviders, + EmailProviderIdEnum, + emailProviders, + InAppProviderIdEnum, + inAppProviders, + pushProviders, + SmsProviderIdEnum, + smsProviders, +} from '@novu/shared'; +import { test, expect } from '@playwright/test'; + +import { getByTestId, initializeSession, isDarkTheme } from './utils.ts/browser'; +import { checkTableLoading, checkTableRow, clickOnListRow, navigateToGetStarted } from './utils.ts/integrations'; +import { deleteProvider } from './utils.ts/plugins'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context); +}); + +test('should show the table loading skeleton and empty state', async ({ page }) => { + const integrationsPromise = page.route('**/integrations', async (route) => { + const response = await page.request.fetch(route.request()); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + await route.fulfill({ + response, + body: JSON.stringify({ data: [] }), + }); + }); + + await navigateToGetStarted(page, 'channel-card-sms'); + + const providerSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(providerSidebar).toBeVisible(); + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + await sidebarClose.click(); + + await checkTableLoading(page); + await integrationsPromise; + + const noIntegrationsPlaceholder = getByTestId(page, 'no-integrations-placeholder'); + await expect(noIntegrationsPlaceholder).toBeVisible(); + await expect(noIntegrationsPlaceholder).toContainText('Choose a channel you want to start sending notifications'); + + const inAppCard = getByTestId(page, 'integration-channel-card-in_app'); + await expect(inAppCard).toBeEnabled(); + await expect(inAppCard).toContainText('In-App'); + const emailCard = getByTestId(page, 'integration-channel-card-email'); + await expect(emailCard).toBeEnabled(); + await expect(emailCard).toContainText('Email'); + const chatCard = getByTestId(page, 'integration-channel-card-chat'); + await expect(chatCard).toBeEnabled(); + await expect(chatCard).toContainText('Chat'); + const pushCard = getByTestId(page, 'integration-channel-card-push'); + await expect(pushCard).toBeEnabled(); + await expect(pushCard).toContainText('Push'); + const smsCard = getByTestId(page, 'integration-channel-card-sms'); + await expect(smsCard).toBeEnabled(); + await expect(smsCard).toContainText('SMS'); +}); + +test('should show the table loading skeleton and then table', async ({ page }) => { + const integrationsPromise = page.route('**/integrations', async (route) => { + await new Promise((resolve) => setTimeout(resolve, 2000)); + const response = await page.request.fetch(route.request()); + const body = await response.json(); + + await route.fulfill({ + response, + body: JSON.stringify(body), + }); + }); + + await navigateToGetStarted(page, 'channel-card-sms'); + + const providerSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(providerSidebar).toBeVisible(); + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + await sidebarClose.click(); + + await checkTableLoading(page); + await integrationsPromise; + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await expect(addProvider).toContainText('Add a provider'); + + await checkTableRow(page, { + name: 'SendGrid', + provider: 'SendGrid', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Twilio', + provider: 'Twilio', + channel: 'SMS', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Slack', + provider: 'Slack', + channel: 'Chat', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Discord', + provider: 'Discord', + channel: 'Chat', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Firebase Cloud Messaging', + provider: 'Firebase Cloud Messaging', + channel: 'Push', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Novu In-App', + isFree: false, + provider: 'Novu In-App', + channel: 'In-App', + environment: 'Development', + status: 'Active', + }); +}); + +test('should show the select provider sidebar', async ({ page }) => { + await deleteProvider({ + providerId: InAppProviderIdEnum.Novu, + channel: ChannelTypeEnum.IN_APP, + environmentId: session.environment.id, + organizationId: session.organization.id, + }); + + await navigateToGetStarted(page); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + await expect(selectProviderSidebar).toContainText('Select a provider'); + await expect(selectProviderSidebar).toContainText('Select a provider to create instance for a channel'); + const search = selectProviderSidebar.locator('input[type="search"]'); + await expect(search).toHaveAttribute('placeholder', 'Search a provider...'); + const sidebarClose = getByTestId(selectProviderSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const activeTab = channelTabs.locator('[data-active="true"]'); + await expect(activeTab).toContainText('Email'); + await expect(channelTabs).toContainText('In-App'); + await expect(channelTabs).toContainText('Email'); + await expect(channelTabs).toContainText('Chat'); + await expect(channelTabs).toContainText('Push'); + await expect(channelTabs).toContainText('SMS'); + + const inAppGroup = getByTestId(page, 'providers-group-in_app'); + await expect(inAppGroup).toContainText('In-App'); + const emailGroup = getByTestId(page, 'providers-group-email'); + await expect(emailGroup).toContainText('Email'); + const chatGroup = getByTestId(page, 'providers-group-chat'); + await expect(chatGroup).toContainText('Chat'); + const pushGroup = getByTestId(page, 'providers-group-push'); + await expect(pushGroup).toContainText('Push'); + const smsGroup = getByTestId(page, 'providers-group-sms'); + await expect(smsGroup).toContainText('SMS'); + + const allProviders = inAppProviders.concat(emailProviders, chatProviders, pushProviders, smsProviders); + for (const provider of allProviders) { + if (provider.id === EmailProviderIdEnum.Novu || provider.id === SmsProviderIdEnum.Novu) { + continue; + } + + const providerInGroup = getByTestId(selectProviderSidebar, `provider-${provider.id}`); + await expect(providerInGroup).toContainText(provider.displayName); + } + + const cancel = getByTestId(selectProviderSidebar, 'select-provider-sidebar-cancel'); + await expect(cancel).toContainText('Cancel'); + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should allow for searching', async ({ page }) => { + await navigateToGetStarted(page); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const search = selectProviderSidebar.locator('input[type="search"]'); + await search.fill('Mail'); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const inAppTab = channelTabs.locator('button', { hasText: 'In-App' }); + await expect(inAppTab).toBeHidden(); + const emailTab = channelTabs.locator('button', { hasText: 'Email' }); + await expect(emailTab).toBeVisible(); + const chatTab = channelTabs.locator('button', { hasText: 'Chat' }); + await expect(chatTab).toBeHidden(); + const pushTab = channelTabs.locator('button', { hasText: 'Push' }); + await expect(pushTab).toBeHidden(); + const smsTab = channelTabs.locator('button', { hasText: 'SMS' }); + await expect(smsTab).toBeHidden(); + + const emailGroup = getByTestId(page, 'providers-group-email'); + await expect(emailGroup).toContainText('Email'); + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + const mailgun = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailgun}`); + await expect(mailgun).toContainText('Mailgun'); + const mailerSend = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.MailerSend}`); + await expect(mailerSend).toContainText('MailerSend'); + const emailWebhook = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.EmailWebhook}`); + await expect(emailWebhook).toContainText('Email Webhook'); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should show empty search results', async ({ page }) => { + await navigateToGetStarted(page); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const search = selectProviderSidebar.locator('input[type="search"]'); + await search.fill('safasdfasdfasdfasdfas'); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const inAppTab = channelTabs.locator('button', { hasText: 'In-App' }); + await expect(inAppTab).toBeHidden(); + const emailTab = channelTabs.locator('button', { hasText: 'Email' }); + await expect(emailTab).toBeHidden(); + const chatTab = channelTabs.locator('button', { hasText: 'Chat' }); + await expect(chatTab).toBeHidden(); + const pushTab = channelTabs.locator('button', { hasText: 'Push' }); + await expect(pushTab).toBeHidden(); + const smsTab = channelTabs.locator('button', { hasText: 'SMS' }); + await expect(smsTab).toBeHidden(); + + const noSearchResults = getByTestId(page, 'select-provider-no-search-results-img'); + await expect(noSearchResults).toBeVisible(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should allow selecting a provider', async ({ page }) => { + await navigateToGetStarted(page); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const selectedProviderImage = getByTestId( + selectProviderSidebar, + `selected-provider-image-${EmailProviderIdEnum.Mailjet}` + ).first(); + const isDarkThemeEnabled = await isDarkTheme(page); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${EmailProviderIdEnum.Mailjet}.svg` + ); + + const selectedProviderName = getByTestId(selectProviderSidebar, 'selected-provider-name'); + await expect(selectedProviderName).toContainText('Mailjet'); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeEnabled(); +}); + +test('should allow moving to create sidebar', async ({ page }) => { + await navigateToGetStarted(page); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const createProviderInstanceSidebar = getByTestId(page, 'create-provider-instance-sidebar'); + await expect(createProviderInstanceSidebar).toBeVisible(); + await expect(createProviderInstanceSidebar).toContainText( + 'Specify assignment preferences to automatically allocate the provider instance to the Email channel.' + ); + await expect(createProviderInstanceSidebar).toContainText('Environment'); + await expect(createProviderInstanceSidebar).toContainText('Provider instance executes only for'); + + const sidebarClose = getByTestId(createProviderInstanceSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const selectedProviderImage = getByTestId( + createProviderInstanceSidebar, + `selected-provider-image-${EmailProviderIdEnum.Mailjet}` + ).first(); + const isDarkThemeEnabled = await isDarkTheme(page); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${EmailProviderIdEnum.Mailjet}.svg` + ); + + const selectedProviderName = getByTestId(createProviderInstanceSidebar, 'provider-instance-name'); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Mailjet'); + + const environmentRadios = createProviderInstanceSidebar.locator('[role="radiogroup"]'); + const selectedEnv = environmentRadios.locator('[data-checked="true"]'); + await expect(selectedEnv).toContainText('Development'); + await expect(environmentRadios).toContainText('Production'); + + const cancel = getByTestId(createProviderInstanceSidebar, 'create-provider-instance-sidebar-cancel'); + await expect(cancel).toContainText('Cancel'); + await expect(cancel).toBeEnabled(); + + const create = getByTestId(createProviderInstanceSidebar, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); +}); + +test('should allow moving back from create provider sidebar to select provider sidebar', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const back = getByTestId(page, 'create-provider-instance-sidebar-back'); + await expect(back).toBeVisible(); + await back.click(); + + selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); +}); + +test('should create a new mailjet integration', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const close = getByTestId(updateProviderSidebar, 'sidebar-close'); + await expect(close).toBeVisible(); + await close.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Disabled', + }); +}); + +test('should update the mailjet integration', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + await expect(updateProviderSidebar).toContainText('Set up credentials to start sending notifications.'); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('Email'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('false'); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await expect(providerName).toHaveValue('Mailjet Integration'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/mailjet/); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toBeDisabled(); + + await providerName.clear(); + await providerName.fill('Mailjet Integration Updated'); + + await isActive.locator('~ label').click(); + + const apiKey = getByTestId(updateProviderSidebar, 'apiKey'); + await apiKey.fill('fake-api-key'); + + const secretKey = getByTestId(updateProviderSidebar, 'secretKey'); + await secretKey.fill('fake-secret-key'); + + const fromField = getByTestId(updateProviderSidebar, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(updateProviderSidebar, 'senderName'); + await senderName.fill('Novu'); + + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const modalClose = page.locator('.mantine-Modal-close'); + await modalClose.click(); + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration Updated', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); +}); + +test('should update the mailjet integration from the list', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, 'Mailjet Integration'); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('false'); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await expect(providerName).toHaveValue('Mailjet Integration'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/mailjet/); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toBeDisabled(); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration Updated'); + + await isActive.locator('~ label').click(); + + const apiKey = getByTestId(updateProviderSidebar, 'apiKey'); + await apiKey.fill('fake-api-key'); + + const secretKey = getByTestId(updateProviderSidebar, 'secretKey'); + await secretKey.fill('fake-secret-key'); + + const fromField = getByTestId(updateProviderSidebar, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(updateProviderSidebar, 'senderName'); + await senderName.fill('Novu'); + + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const modalClose = page.locator('.mantine-Modal-close'); + await modalClose.click(); + sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration Updated', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); +}); + +test('should allow to delete the mailjet integration', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, 'Mailjet Integration'); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const menu = updateProviderSidebar.locator('[aria-haspopup="menu"]'); + await menu.click(); + const deleteButton = updateProviderSidebar.locator('button[data-menu-item="true"]', { hasText: 'Delete' }); + await deleteButton.click(); + + const deleteModal = getByTestId(page, 'delete-provider-instance-modal'); + await expect(deleteModal).toBeVisible(); + await expect(deleteModal).toContainText('Delete Mailjet Integration instance?'); + await expect(deleteModal).toContainText( + 'Deleting a provider instance will fail workflows relying on its configuration, leading to undelivered notifications.' + ); + + const cancel = deleteModal.getByRole('button', { name: 'Cancel' }); + await expect(cancel).toBeEnabled(); + const deleteInstanceButton = deleteModal.getByRole('button', { name: 'Delete instance' }); + await expect(deleteInstanceButton).toBeEnabled(); + await deleteInstanceButton.click(); + + const integrationsTable = getByTestId(page, 'integration-name-cell', { hasText: 'Mailjet Integration' }); + await expect(integrationsTable).toBeHidden(); +}); + +test('should show the Novu in-app integration', async ({ page }) => { + await navigateToGetStarted(page); + + await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + await expect(updateProviderSidebar).toContainText( + 'Select a framework to set up credentials to start sending notifications.' + ); + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('In-App'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const linkToDocs = updateProviderSidebar.getByRole('link', { name: 'Explore set-up guide' }); + await expect(linkToDocs).toBeVisible(); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('true'); + + const isDarkThemeEnabled = await isDarkTheme(page); + const selectedProviderImage = getByTestId( + updateProviderSidebar, + `selected-provider-image-${InAppProviderIdEnum.Novu}` + ); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${InAppProviderIdEnum.Novu}.svg` + ); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu In-App'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/novu-in-app/); + + const hmacCheckbox = getByTestId(updateProviderSidebar, 'hmac'); + await expect(hmacCheckbox).not.toBeChecked(); + + const novuInAppFrameworks = getByTestId(updateProviderSidebar, 'novu-in-app-frameworks'); + await expect(novuInAppFrameworks).toContainText('Integrate In-App using a framework below'); + await expect(novuInAppFrameworks).toContainText('React'); + await expect(novuInAppFrameworks).toContainText('Angular'); + await expect(novuInAppFrameworks).toContainText('Web Component'); + await expect(novuInAppFrameworks).toContainText('Headless'); + await expect(novuInAppFrameworks).toContainText('Vue'); + await expect(novuInAppFrameworks).toContainText('iFrame'); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeDisabled(); +}); + +test('should show the Novu in-app integration - React guide', async ({ page }) => { + await navigateToGetStarted(page); + + await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const novuInAppFrameworks = getByTestId(updateProviderSidebar, 'novu-in-app-frameworks'); + await expect(novuInAppFrameworks).toContainText('React'); + + const reactGuide = novuInAppFrameworks.locator('div').filter({ hasText: 'React' }).nth(1); + await reactGuide.click(); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toContainText('React integration guide'); + + const sidebarBack = getByTestId(updateProviderSidebar, 'sidebar-back'); + await expect(sidebarBack).toBeVisible(); + const setupTimeline = getByTestId(updateProviderSidebar, 'setup-timeline'); + await expect(setupTimeline).toBeVisible(); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeDisabled(); +}); + +test('should show the Novu Email integration sidebar', async ({ page }) => { + const integrationsPromise = page.route('**/integrations', async (route) => { + const response = await page.request.fetch(route.request()); + const body = await response.json(); + const [firstIntegration] = body.data; + body.data = [ + { + _id: EmailProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: EmailProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.EMAIL, + name: 'Novu Email', + identifier: EmailProviderIdEnum.Novu, + }, + ...body.data, + ]; + + await route.fulfill({ + response, + body: JSON.stringify(body), + }); + }); + + await navigateToGetStarted(page); + await integrationsPromise; + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, new RegExp(`Novu Email.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar-novu'); + await expect(updateProviderSidebar).toContainText('Test Provider'); + await expect(updateProviderSidebar).toBeVisible(); + + const isDarkThemeEnabled = await isDarkTheme(page); + const novuEmailLogo = updateProviderSidebar.locator( + `img[src="/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${ + EmailProviderIdEnum.Novu + }.svg"]` + ); + await expect(novuEmailLogo).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('Email'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu Email'); + + const providerLimits = getByTestId(updateProviderSidebar, 'novu-provider-limits'); + const providerLimitsText = await providerLimits.innerText(); + await expect(providerLimitsText).toEqual( + 'Novu provider allows sending max 300 emails per month,\nto send more messages, configure a different provider' + ); + + const limitbarLimit = getByTestId(updateProviderSidebar, 'limitbar-limit'); + const limitbarText = await limitbarLimit.innerText(); + await expect(limitbarText).toEqual('300 emails per month'); +}); + +test('should show the Novu SMS integration sidebar', async ({ page }) => { + const integrationsPromise = page.route('**/integrations', async (route) => { + const response = await page.request.fetch(route.request()); + const body = await response.json(); + const [firstIntegration] = body.data; + body.data = [ + { + _id: SmsProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: SmsProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.SMS, + name: 'Novu SMS', + identifier: SmsProviderIdEnum.Novu, + }, + ...body.data, + ]; + + await route.fulfill({ + response, + body: JSON.stringify(body), + }); + }); + + await navigateToGetStarted(page); + await integrationsPromise; + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, new RegExp(`Novu SMS.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar-novu'); + await expect(updateProviderSidebar).toContainText('Test Provider'); + await expect(updateProviderSidebar).toBeVisible(); + + const isDarkThemeEnabled = await isDarkTheme(page); + const novuEmailLogo = updateProviderSidebar.locator( + `img[src="/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${SmsProviderIdEnum.Novu}.svg"]` + ); + await expect(novuEmailLogo).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('SMS'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu SMS'); + + const providerLimits = getByTestId(updateProviderSidebar, 'novu-provider-limits'); + const providerLimitsText = await providerLimits.innerText(); + await expect(providerLimitsText).toEqual( + 'Novu provider allows sending max 20 messages per month,\nto send more messages, configure a different provider' + ); + + const limitbarLimit = getByTestId(updateProviderSidebar, 'limitbar-limit'); + const limitbarText = await limitbarLimit.innerText(); + await expect(limitbarText).toEqual('20 messages per month'); +}); diff --git a/apps/web/tests/integrations-list-page.spec.ts b/apps/web/tests/integrations-list-page.spec.ts new file mode 100644 index 00000000000..c77de8fb242 --- /dev/null +++ b/apps/web/tests/integrations-list-page.spec.ts @@ -0,0 +1,1116 @@ +import { + ChannelTypeEnum, + chatProviders, + EmailProviderIdEnum, + emailProviders, + InAppProviderIdEnum, + inAppProviders, + pushProviders, + SmsProviderIdEnum, + smsProviders, +} from '@novu/shared'; +import { test, expect } from '@playwright/test'; + +import { getByTestId, initializeSession, isDarkTheme } from './utils.ts/browser'; +import { checkTableLoading, checkTableRow, clickOnListRow, navigateToGetStarted } from './utils.ts/integrations'; +import { deleteProvider } from './utils.ts/plugins'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context); +}); + +test('should show the table loading skeleton and empty state', async ({ page }) => { + const integrationsPromise = page.route('**/v1/integrations', async (route) => { + const response = await page.request.fetch(route.request()); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + await route.fulfill({ + response, + body: JSON.stringify({ data: [] }), + }); + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await checkTableLoading(page); + await integrationsPromise; + + const noIntegrationsPlaceholder = getByTestId(page, 'no-integrations-placeholder'); + await expect(noIntegrationsPlaceholder).toBeVisible(); + await expect(noIntegrationsPlaceholder).toContainText('Choose a channel you want to start sending notifications'); + + const inAppCard = getByTestId(page, 'integration-channel-card-in_app'); + await expect(inAppCard).toBeEnabled(); + await expect(inAppCard).toContainText('In-App'); + const emailCard = getByTestId(page, 'integration-channel-card-email'); + await expect(emailCard).toBeEnabled(); + await expect(emailCard).toContainText('Email'); + const chatCard = getByTestId(page, 'integration-channel-card-chat'); + await expect(chatCard).toBeEnabled(); + await expect(chatCard).toContainText('Chat'); + const pushCard = getByTestId(page, 'integration-channel-card-push'); + await expect(pushCard).toBeEnabled(); + await expect(pushCard).toContainText('Push'); + const smsCard = getByTestId(page, 'integration-channel-card-sms'); + await expect(smsCard).toBeEnabled(); + await expect(smsCard).toContainText('SMS'); +}); + +test('should show the table loading skeleton and then table', async ({ page }) => { + const integrationsPromise = page.route('**/v1/integrations', async (route) => { + await new Promise((resolve) => setTimeout(resolve, 2000)); + const response = await page.request.fetch(route.request()); + const body = await response.json(); + + await route.fulfill({ + response, + body: JSON.stringify(body), + }); + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await checkTableLoading(page); + await integrationsPromise; + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await expect(addProvider).toContainText('Add a provider'); + + await checkTableRow(page, { + name: 'SendGrid', + provider: 'SendGrid', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Twilio', + provider: 'Twilio', + channel: 'SMS', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Slack', + provider: 'Slack', + channel: 'Chat', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Discord', + provider: 'Discord', + channel: 'Chat', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Firebase Cloud Messaging', + provider: 'Firebase Cloud Messaging', + channel: 'Push', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Novu In-App', + isFree: false, + provider: 'Novu In-App', + channel: 'In-App', + environment: 'Development', + status: 'Active', + }); +}); + +test('should show the select provider sidebar', async ({ page }) => { + await deleteProvider({ + providerId: InAppProviderIdEnum.Novu, + channel: ChannelTypeEnum.IN_APP, + environmentId: session.environment.id, + organizationId: session.organization.id, + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + await expect(selectProviderSidebar).toContainText('Select a provider'); + await expect(selectProviderSidebar).toContainText('Select a provider to create instance for a channel'); + const search = selectProviderSidebar.locator('input[type="search"]'); + await expect(search).toHaveAttribute('placeholder', 'Search a provider...'); + const sidebarClose = getByTestId(selectProviderSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const activeTab = channelTabs.locator('[data-active="true"]'); + await expect(activeTab).toContainText('In-App'); + await expect(channelTabs).toContainText('In-App'); + await expect(channelTabs).toContainText('Email'); + await expect(channelTabs).toContainText('Chat'); + await expect(channelTabs).toContainText('Push'); + await expect(channelTabs).toContainText('SMS'); + + const inAppGroup = getByTestId(page, 'providers-group-in_app'); + await expect(inAppGroup).toContainText('In-App'); + const emailGroup = getByTestId(page, 'providers-group-email'); + await expect(emailGroup).toContainText('Email'); + const chatGroup = getByTestId(page, 'providers-group-chat'); + await expect(chatGroup).toContainText('Chat'); + const pushGroup = getByTestId(page, 'providers-group-push'); + await expect(pushGroup).toContainText('Push'); + const smsGroup = getByTestId(page, 'providers-group-sms'); + await expect(smsGroup).toContainText('SMS'); + + const allProviders = inAppProviders.concat(emailProviders, chatProviders, pushProviders, smsProviders); + for (const provider of allProviders) { + if (provider.id === EmailProviderIdEnum.Novu || provider.id === SmsProviderIdEnum.Novu) { + continue; + } + + const providerInGroup = getByTestId(selectProviderSidebar, `provider-${provider.id}`); + await expect(providerInGroup).toContainText(provider.displayName); + } + + const cancel = getByTestId(selectProviderSidebar, 'select-provider-sidebar-cancel'); + await expect(cancel).toContainText('Cancel'); + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should allow for searching', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const search = selectProviderSidebar.locator('input[type="search"]'); + await search.fill('Mail'); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const inAppTab = channelTabs.locator('button', { hasText: 'In-App' }); + await expect(inAppTab).toBeHidden(); + const emailTab = channelTabs.locator('button', { hasText: 'Email' }); + await expect(emailTab).toBeVisible(); + const chatTab = channelTabs.locator('button', { hasText: 'Chat' }); + await expect(chatTab).toBeHidden(); + const pushTab = channelTabs.locator('button', { hasText: 'Push' }); + await expect(pushTab).toBeHidden(); + const smsTab = channelTabs.locator('button', { hasText: 'SMS' }); + await expect(smsTab).toBeHidden(); + + const emailGroup = getByTestId(page, 'providers-group-email'); + await expect(emailGroup).toContainText('Email'); + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + const mailgun = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailgun}`); + await expect(mailgun).toContainText('Mailgun'); + const mailerSend = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.MailerSend}`); + await expect(mailerSend).toContainText('MailerSend'); + const emailWebhook = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.EmailWebhook}`); + await expect(emailWebhook).toContainText('Email Webhook'); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should show empty search results', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const search = selectProviderSidebar.locator('input[type="search"]'); + await search.fill('safasdfasdfasdfasdfas'); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const inAppTab = channelTabs.locator('button', { hasText: 'In-App' }); + await expect(inAppTab).toBeHidden(); + const emailTab = channelTabs.locator('button', { hasText: 'Email' }); + await expect(emailTab).toBeHidden(); + const chatTab = channelTabs.locator('button', { hasText: 'Chat' }); + await expect(chatTab).toBeHidden(); + const pushTab = channelTabs.locator('button', { hasText: 'Push' }); + await expect(pushTab).toBeHidden(); + const smsTab = channelTabs.locator('button', { hasText: 'SMS' }); + await expect(smsTab).toBeHidden(); + + const noSearchResults = getByTestId(page, 'select-provider-no-search-results-img'); + await expect(noSearchResults).toBeVisible(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should allow selecting a provider', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const selectedProviderImage = getByTestId( + selectProviderSidebar, + `selected-provider-image-${EmailProviderIdEnum.Mailjet}` + ).first(); + const isDarkThemeEnabled = await isDarkTheme(page); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${EmailProviderIdEnum.Mailjet}.svg` + ); + + const selectedProviderName = getByTestId(selectProviderSidebar, 'selected-provider-name'); + await expect(selectedProviderName).toContainText('Mailjet'); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeEnabled(); +}); + +test('should allow moving to create sidebar', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const createProviderInstanceSidebar = getByTestId(page, 'create-provider-instance-sidebar'); + await expect(createProviderInstanceSidebar).toBeVisible(); + await expect(createProviderInstanceSidebar).toContainText( + 'Specify assignment preferences to automatically allocate the provider instance to the Email channel.' + ); + await expect(createProviderInstanceSidebar).toContainText('Environment'); + await expect(createProviderInstanceSidebar).toContainText('Provider instance executes only for'); + + const sidebarClose = getByTestId(createProviderInstanceSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const selectedProviderImage = getByTestId( + createProviderInstanceSidebar, + `selected-provider-image-${EmailProviderIdEnum.Mailjet}` + ).first(); + const isDarkThemeEnabled = await isDarkTheme(page); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${EmailProviderIdEnum.Mailjet}.svg` + ); + + const selectedProviderName = getByTestId(createProviderInstanceSidebar, 'provider-instance-name'); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Mailjet'); + + const environmentRadios = createProviderInstanceSidebar.locator('[role="radiogroup"]'); + const selectedEnv = environmentRadios.locator('[data-checked="true"]'); + await expect(selectedEnv).toContainText('Development'); + await expect(environmentRadios).toContainText('Production'); + + const cancel = getByTestId(createProviderInstanceSidebar, 'create-provider-instance-sidebar-cancel'); + await expect(cancel).toContainText('Cancel'); + await expect(cancel).toBeEnabled(); + + const create = getByTestId(createProviderInstanceSidebar, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); +}); + +test('should allow moving back from create provider sidebar to select provider sidebar', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const back = getByTestId(page, 'create-provider-instance-sidebar-back'); + await expect(back).toBeVisible(); + await back.click(); + + selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); +}); + +test('should create a new mailjet integration', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const close = getByTestId(updateProviderSidebar, 'sidebar-close'); + await expect(close).toBeVisible(); + await close.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Disabled', + }); +}); + +test('should create a new mailjet integration with conditions', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const addConditionsButton = getByTestId(page, 'add-conditions-btn'); + await addConditionsButton.click(); + + const conditionsTitle = getByTestId(page, 'conditions-form-title'); + await expect(conditionsTitle).toContainText('Conditions for Mailjet Integration provider instance'); + + const addConditionButton = getByTestId(page, 'add-new-condition'); + await addConditionButton.click(); + + const formOn = getByTestId(page, 'conditions-form-on'); + await expect(formOn).toHaveValue('Tenant'); + + const formKey = getByTestId(page, 'conditions-form-key'); + await formKey.fill('identifier'); + + const formOperator = getByTestId(page, 'conditions-form-operator'); + await expect(formOperator).toHaveValue('Equal'); + + const formValue = getByTestId(page, 'conditions-form-value'); + await formValue.fill('tenant123'); + + let applyButton = getByTestId(page, 'apply-conditions-btn'); + await applyButton.click(); + + await expect(addConditionsButton).toContainText('Edit conditions'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let headerAddConditions = getByTestId(updateProviderSidebar, 'header-add-conditions-btn'); + await expect(headerAddConditions).toContainText('1'); + await headerAddConditions.click(); + + const addNewCondition = getByTestId(page, 'add-new-condition'); + await addNewCondition.click(); + + const conditionsFormKey = getByTestId(page, 'conditions-form-key').last(); + await conditionsFormKey.fill('identifier'); + + const conditionsFormValue = getByTestId(page, 'conditions-form-value').last(); + await conditionsFormValue.fill('tenant456'); + + applyButton = getByTestId(page, 'apply-conditions-btn'); + await applyButton.click(); + + headerAddConditions = getByTestId(updateProviderSidebar, 'header-add-conditions-btn'); + await expect(headerAddConditions).toContainText('2'); + + await expect(page).toHaveURL(/\/integrations\//); + + const close = getByTestId(updateProviderSidebar, 'sidebar-close'); + await expect(close).toBeVisible(); + await close.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Disabled', + }); +}); + +test('should remove as primary when adding conditions', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await clickOnListRow(page, new RegExp(`SendGrid.*Development`)); + + const headerAddConditions = getByTestId(page, 'header-add-conditions-btn'); + await headerAddConditions.click(); + + const removePrimaryFlagModal = getByTestId(page, 'remove-primary-flag-modal'); + await expect(removePrimaryFlagModal).toBeVisible(); + await expect(removePrimaryFlagModal).toContainText('Primary flag will be removed'); + await expect(removePrimaryFlagModal).toContainText( + 'Adding conditions to the primary provider instance removes its primary status when a user applies changes by' + ); + + const cancel = removePrimaryFlagModal.getByRole('button', { name: 'Cancel' }); + await expect(cancel).toBeVisible(); + + const gotIt = removePrimaryFlagModal.getByRole('button', { name: 'Got it' }); + await expect(gotIt).toBeVisible(); + await gotIt.click(); + + const conditionsTitle = getByTestId(page, 'conditions-form-title'); + await expect(conditionsTitle).toContainText('Conditions for SendGrid provider instance'); + + const addConditionButton = getByTestId(page, 'add-new-condition'); + await addConditionButton.click(); + + const formOn = getByTestId(page, 'conditions-form-on'); + await expect(formOn).toHaveValue('Tenant'); + + const formKey = getByTestId(page, 'conditions-form-key'); + await formKey.fill('identifier'); + + const formOperator = getByTestId(page, 'conditions-form-operator'); + await expect(formOperator).toHaveValue('Equal'); + + const formValue = getByTestId(page, 'conditions-form-value'); + await formValue.fill('tenant123'); + + let applyButton = getByTestId(page, 'apply-conditions-btn'); + await applyButton.click(); + + const providerName = page.getByPlaceholder('Enter instance name'); + await providerName.clear(); + await providerName.fill('SendGrid test'); + + const fromField = getByTestId(page, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(page, 'senderName'); + await senderName.fill('Novu'); + + const updateButton = getByTestId(page, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const makePrimaryButton = page.locator('button', { hasText: 'Make primary' }); + await expect(makePrimaryButton).toBeVisible(); + + const closeButton = page.locator('.mantine-Modal-close'); + await closeButton.click(); +}); + +test('should remove conditions when set to primary', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + await expect(page).toHaveURL(/\/integrations\/create/); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + await expect(page).toHaveURL(/\/integrations\/create\/email\/mailjet/); + + const providerName = page.getByPlaceholder('Enter instance name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const addConditionsButton = getByTestId(page, 'add-conditions-btn'); + await addConditionsButton.click(); + + const conditionsTitle = getByTestId(page, 'conditions-form-title'); + await expect(conditionsTitle).toContainText('Conditions for Mailjet Integration provider instance'); + + const addNewCondition = getByTestId(page, 'add-new-condition'); + await addNewCondition.click(); + + const formOn = getByTestId(page, 'conditions-form-on'); + await expect(formOn).toHaveValue('Tenant'); + + const formKey = getByTestId(page, 'conditions-form-key'); + await formKey.fill('identifier'); + + const formOperator = getByTestId(page, 'conditions-form-operator'); + await expect(formOperator).toHaveValue('Equal'); + + const formValue = getByTestId(page, 'conditions-form-value'); + await formValue.fill('tenant123'); + + let applyButton = getByTestId(page, 'apply-conditions-btn'); + await applyButton.click(); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const headerAddConditions = getByTestId(updateProviderSidebar, 'header-add-conditions-btn'); + await expect(headerAddConditions).toContainText('1'); + + let makePrimaryButton = getByTestId(updateProviderSidebar, 'header-make-primary-btn'); + await makePrimaryButton.click(); + + const removeConditionsModal = getByTestId(page, 'remove-conditions-modal'); + await expect(removeConditionsModal).toBeVisible(); + await expect(removeConditionsModal).toContainText('Conditions will be removed'); + await expect(removeConditionsModal).toContainText('Marking this instance as primary will remove all conditions'); + const cancel = removeConditionsModal.getByRole('button', { name: 'Cancel' }); + await expect(cancel).toBeVisible(); + const removeConditionsButton = removeConditionsModal.getByRole('button', { name: 'Remove conditions' }); + await expect(removeConditionsButton).toBeVisible(); + await removeConditionsButton.click(); + + makePrimaryButton = getByTestId(updateProviderSidebar, 'header-make-primary-btn'); + await expect(makePrimaryButton).toBeHidden(); + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, new RegExp(`Mailjet Integration.*Development`)); +}); + +test('should update the mailjet integration', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + await expect(updateProviderSidebar).toContainText('Set up credentials to start sending notifications.'); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('Email'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('false'); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await expect(providerName).toHaveValue('Mailjet Integration'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/mailjet/); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toBeDisabled(); + + await providerName.clear(); + await providerName.fill('Mailjet Integration Updated'); + + await isActive.locator('~ label').click(); + + const apiKey = getByTestId(updateProviderSidebar, 'apiKey'); + await apiKey.fill('fake-api-key'); + + const secretKey = getByTestId(updateProviderSidebar, 'secretKey'); + await secretKey.fill('fake-secret-key'); + + const fromField = getByTestId(updateProviderSidebar, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(updateProviderSidebar, 'senderName'); + await senderName.fill('Novu'); + + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const modalClose = page.locator('.mantine-Modal-close'); + await modalClose.click(); + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration Updated', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); +}); + +test('should update the mailjet integration from the list', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, 'Mailjet Integration'); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('false'); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await expect(providerName).toHaveValue('Mailjet Integration'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/mailjet/); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toBeDisabled(); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration Updated'); + + await isActive.locator('~ label').click(); + + const apiKey = getByTestId(updateProviderSidebar, 'apiKey'); + await apiKey.fill('fake-api-key'); + + const secretKey = getByTestId(updateProviderSidebar, 'secretKey'); + await secretKey.fill('fake-secret-key'); + + const fromField = getByTestId(updateProviderSidebar, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(updateProviderSidebar, 'senderName'); + await senderName.fill('Novu'); + + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const modalClose = page.locator('.mantine-Modal-close'); + await modalClose.click(); + sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration Updated', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); +}); + +test('should allow to delete the mailjet integration', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, 'Mailjet Integration'); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const menu = updateProviderSidebar.locator('[aria-haspopup="menu"]'); + await menu.click(); + const deleteButton = updateProviderSidebar.locator('button[data-menu-item="true"]', { hasText: 'Delete' }); + await deleteButton.click(); + + const deleteModal = getByTestId(page, 'delete-provider-instance-modal'); + await expect(deleteModal).toBeVisible(); + await expect(deleteModal).toContainText('Delete Mailjet Integration instance?'); + await expect(deleteModal).toContainText( + 'Deleting a provider instance will fail workflows relying on its configuration, leading to undelivered notifications.' + ); + + const cancel = deleteModal.getByRole('button', { name: 'Cancel' }); + await expect(cancel).toBeEnabled(); + const deleteInstanceButton = deleteModal.getByRole('button', { name: 'Delete instance' }); + await expect(deleteInstanceButton).toBeEnabled(); + await deleteInstanceButton.click(); + + const integrationsTable = getByTestId(page, 'integration-name-cell', { hasText: 'Mailjet Integration' }); + await expect(integrationsTable).toBeHidden(); +}); + +test('should show the Novu in-app integration', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + await expect(updateProviderSidebar).toContainText( + 'Select a framework to set up credentials to start sending notifications.' + ); + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('In-App'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const linkToDocs = updateProviderSidebar.getByRole('link', { name: 'Explore set-up guide' }); + await expect(linkToDocs).toBeVisible(); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('true'); + + const isDarkThemeEnabled = await isDarkTheme(page); + const selectedProviderImage = getByTestId( + updateProviderSidebar, + `selected-provider-image-${InAppProviderIdEnum.Novu}` + ); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${InAppProviderIdEnum.Novu}.svg` + ); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu In-App'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/novu-in-app/); + + const hmacCheckbox = getByTestId(updateProviderSidebar, 'hmac'); + await expect(hmacCheckbox).not.toBeChecked(); + + const novuInAppFrameworks = getByTestId(updateProviderSidebar, 'novu-in-app-frameworks'); + await expect(novuInAppFrameworks).toContainText('Integrate In-App using a framework below'); + await expect(novuInAppFrameworks).toContainText('React'); + await expect(novuInAppFrameworks).toContainText('Angular'); + await expect(novuInAppFrameworks).toContainText('Web Component'); + await expect(novuInAppFrameworks).toContainText('Headless'); + await expect(novuInAppFrameworks).toContainText('Vue'); + await expect(novuInAppFrameworks).toContainText('iFrame'); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeDisabled(); +}); + +test('should show the Novu in-app integration - React guide', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const novuInAppFrameworks = getByTestId(updateProviderSidebar, 'novu-in-app-frameworks'); + await expect(novuInAppFrameworks).toContainText('React'); + + const reactGuide = novuInAppFrameworks.locator('div').filter({ hasText: 'React' }).nth(1); + await reactGuide.click(); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toContainText('React integration guide'); + + const sidebarBack = getByTestId(updateProviderSidebar, 'sidebar-back'); + await expect(sidebarBack).toBeVisible(); + const setupTimeline = getByTestId(updateProviderSidebar, 'setup-timeline'); + await expect(setupTimeline).toBeVisible(); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeDisabled(); +}); + +test('should show the Novu Email integration sidebar', async ({ page }) => { + const integrationsPromise = page.route('**/v1/integrations', async (route) => { + const response = await page.request.fetch(route.request()); + const body = await response.json(); + const [firstIntegration] = body.data; + body.data = [ + { + _id: EmailProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: EmailProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.EMAIL, + name: 'Novu Email', + identifier: EmailProviderIdEnum.Novu, + }, + ...body.data, + ]; + + await route.fulfill({ + response, + body: JSON.stringify(body), + }); + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + await integrationsPromise; + + await clickOnListRow(page, new RegExp(`Novu Email.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar-novu'); + await expect(updateProviderSidebar).toContainText('Test Provider'); + await expect(updateProviderSidebar).toBeVisible(); + + const isDarkThemeEnabled = await isDarkTheme(page); + const novuEmailLogo = updateProviderSidebar.locator( + `img[src="/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${ + EmailProviderIdEnum.Novu + }.svg"]` + ); + await expect(novuEmailLogo).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('Email'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu Email'); + + const providerLimits = getByTestId(updateProviderSidebar, 'novu-provider-limits'); + const providerLimitsText = await providerLimits.innerText(); + await expect(providerLimitsText).toEqual( + 'Novu provider allows sending max 300 emails per month,\nto send more messages, configure a different provider' + ); + + const limitbarLimit = getByTestId(updateProviderSidebar, 'limitbar-limit'); + const limitbarText = await limitbarLimit.innerText(); + await expect(limitbarText).toEqual('300 emails per month'); +}); + +test('should show the Novu SMS integration sidebar', async ({ page }) => { + const integrationsPromise = page.route('**/v1/integrations', async (route) => { + const response = await page.request.fetch(route.request()); + const body = await response.json(); + const [firstIntegration] = body.data; + body.data = [ + { + _id: SmsProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: SmsProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.SMS, + name: 'Novu SMS', + identifier: SmsProviderIdEnum.Novu, + }, + ...body.data, + ]; + + await route.fulfill({ + response, + body: JSON.stringify(body), + }); + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + await integrationsPromise; + + await clickOnListRow(page, new RegExp(`Novu SMS.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar-novu'); + await expect(updateProviderSidebar).toContainText('Test Provider'); + await expect(updateProviderSidebar).toBeVisible(); + + const isDarkThemeEnabled = await isDarkTheme(page); + const novuEmailLogo = updateProviderSidebar.locator( + `img[src="/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${SmsProviderIdEnum.Novu}.svg"]` + ); + await expect(novuEmailLogo).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('SMS'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu SMS'); + + const providerLimits = getByTestId(updateProviderSidebar, 'novu-provider-limits'); + const providerLimitsText = await providerLimits.innerText(); + await expect(providerLimitsText).toEqual( + 'Novu provider allows sending max 20 messages per month,\nto send more messages, configure a different provider' + ); + + const limitbarLimit = getByTestId(updateProviderSidebar, 'limitbar-limit'); + const limitbarText = await limitbarLimit.innerText(); + await expect(limitbarText).toEqual('20 messages per month'); +}); diff --git a/apps/web/tests/utils.ts/browser.ts b/apps/web/tests/utils.ts/browser.ts index 2b8725e89b0..1985519aa13 100644 --- a/apps/web/tests/utils.ts/browser.ts +++ b/apps/web/tests/utils.ts/browser.ts @@ -1,8 +1,9 @@ import { BrowserContext, Locator, Page } from '@playwright/test'; -import { getSession } from './plugins'; -export async function initializeSession(context: BrowserContext) { - const session = await getSession(); +import { getSession, ISessionOptions } from './plugins'; + +export async function initializeSession(context: BrowserContext, settings: ISessionOptions = {}) { + const session = await getSession(settings); await context.addInitScript((session) => { (window as any).isPlaywright = true; @@ -20,3 +21,11 @@ export async function dragAndDrop(page: Page, dragSelector: string, dropSelector const dndEl = await getByTestId(page, dragSelector); await dndEl.dragTo(await getByTestId(page, dropSelector), { force: true }); } + +export async function isDarkTheme(page: Page) { + const backgroundColor = await page.evaluate(() => { + const body = document.body; + return window.getComputedStyle(body).backgroundColor; + }); + return backgroundColor.toLowerCase() !== '#EDF0F2' && backgroundColor.toLowerCase() !== 'rgb(237, 240, 242)'; +} diff --git a/apps/web/tests/utils.ts/integrations.ts b/apps/web/tests/utils.ts/integrations.ts new file mode 100644 index 00000000000..d4efab64896 --- /dev/null +++ b/apps/web/tests/utils.ts/integrations.ts @@ -0,0 +1,88 @@ +import { expect, Locator, Page } from '@playwright/test'; + +import { getByTestId } from './browser'; + +export const navigateToGetStarted = async (page: Page, card = 'channel-card-email') => { + await page.goto('/get-started'); + await expect(page).toHaveURL(/\/get-started/); + + const cardComponent = getByTestId(page, card); + const button = cardComponent.locator('button'); + await expect(button).toContainText('Change Provider'); + await button.click(); + + const integrationsModal = getByTestId(page, 'integrations-list-modal'); + await expect(integrationsModal).toBeVisible(); + await expect(integrationsModal).toContainText('Integrations Store'); +}; + +export const checkTableLoading = async (page: Page | Locator) => { + const nameCellLoadingElements = getByTestId(page, 'integration-name-cell-loading'); + await expect(nameCellLoadingElements).toHaveCount(10); + await expect(nameCellLoadingElements.first()).toBeVisible(); + + const providerCellLoadingElements = getByTestId(page, 'integration-provider-cell-loading'); + await expect(providerCellLoadingElements).toHaveCount(10); + await expect(providerCellLoadingElements.first()).toBeVisible(); + + const channelCellLoadingElements = getByTestId(page, 'integration-channel-cell-loading'); + await expect(channelCellLoadingElements).toHaveCount(10); + await expect(channelCellLoadingElements.first()).toBeVisible(); + + const envCellLoadingElements = getByTestId(page, 'integration-environment-cell-loading'); + await expect(envCellLoadingElements).toHaveCount(10); + await expect(envCellLoadingElements.first()).toBeVisible(); + + const statusCellLoadingElements = getByTestId(page, 'integration-status-cell-loading'); + await expect(statusCellLoadingElements).toHaveCount(10); + await expect(statusCellLoadingElements.first()).toBeVisible(); +}; + +export const checkTableRow = async ( + page: Page | Locator, + { + name, + isFree, + provider, + channel, + environment, + status, + }: { + name: string; + isFree?: boolean; + provider: string; + channel: string; + environment?: string; + status: string; + } +) => { + const integrationsTable = getByTestId(page, 'integrations-list-table'); + const nthRow = integrationsTable.locator('tbody tr', { hasText: new RegExp(`${name}.*${environment ?? ''}`) }); + const nameCell = getByTestId(nthRow, 'integration-name-cell', { hasText: name }); + await expect(nameCell).toBeVisible(); + + if (isFree) { + await expect(nameCell).toContainText('Test Provider'); + } + + const providerCell = getByTestId(nthRow, 'integration-provider-cell', { hasText: provider }); + await expect(providerCell).toBeVisible(); + + const channelCell = getByTestId(nthRow, 'integration-channel-cell', { hasText: channel }); + await expect(channelCell).toBeVisible(); + + if (environment) { + const environmentCell = getByTestId(nthRow, 'integration-environment-cell', { hasText: environment }); + await expect(environmentCell).toBeVisible(); + } + + const statusCell = getByTestId(nthRow, 'integration-status-cell', { hasText: status }); + await expect(statusCell).toBeVisible(); +}; + +export const clickOnListRow = async (page: Page | Locator, name: string | RegExp) => { + const integrationsTable = getByTestId(page, 'integrations-list-table'); + const row = integrationsTable.locator('tr', { hasText: name }).first(); + await expect(row).toBeVisible(); + await row.click(); +}; diff --git a/apps/web/tests/utils.ts/plugins.ts b/apps/web/tests/utils.ts/plugins.ts index ccca680fb95..513e34f4f44 100644 --- a/apps/web/tests/utils.ts/plugins.ts +++ b/apps/web/tests/utils.ts/plugins.ts @@ -1,14 +1,15 @@ -import { DalService, NotificationTemplateEntity } from '@novu/dal'; +import { DalService, IntegrationRepository, NotificationTemplateEntity } from '@novu/dal'; +import { ChannelTypeEnum, ProvidersIdEnum } from '@novu/shared'; import { UserSession, NotificationTemplateService } from '@novu/testing'; -export async function getSession( - settings: { - noEnvironment?: boolean; - partialTemplate?: Partial; - noTemplates?: boolean; - showOnBoardingTour?: boolean; - } = {} -) { +export interface ISessionOptions { + noEnvironment?: boolean; + partialTemplate?: Partial; + noTemplates?: boolean; + showOnBoardingTour?: boolean; +} + +export async function getSession(settings: ISessionOptions = {}) { const dal = new DalService(); await dal.connect(process.env.MONGODB_URL ?? ''); @@ -49,3 +50,22 @@ export async function getSession( templates, }; } + +export async function deleteProvider(query: { + providerId: ProvidersIdEnum; + channel: ChannelTypeEnum; + environmentId: string; + organizationId: string; +}) { + const dal = new DalService(); + await dal.connect(process.env.MONGODB_URL ?? ''); + + const repository = new IntegrationRepository(); + + return await repository.deleteMany({ + channel: query.channel, + providerId: query.providerId, + _environmentId: query.environmentId, + _organizationId: query.organizationId, + }); +} From 5976fa27a24438e676891cff771d4d3675cbdaed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Fri, 12 Apr 2024 17:04:30 +0200 Subject: [PATCH 16/33] chore(web): fixing cspell --- .cspell.json | 4 +- apps/web/tests-examples/demo-todo-app.spec.ts | 416 ------------------ apps/web/tests/integrations-list-page.spec.ts | 2 +- 3 files changed, 4 insertions(+), 418 deletions(-) delete mode 100644 apps/web/tests-examples/demo-todo-app.spec.ts diff --git a/.cspell.json b/.cspell.json index 78e40221ff3..7f3907b461a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -620,7 +620,8 @@ "uuidv", "Vonage", "runtimes", - "cafebabe" + "cafebabe", + "limitbar", ], "flagWords": [], "patterns": [ @@ -683,6 +684,7 @@ "apps/api/src/.env.test", "apps/ws/src/.env.test", "apps/ws/src/.example.env", + "apps/web/playwright.config.ts", ".cspell.json", "package.json", "package-lock.json", diff --git a/apps/web/tests-examples/demo-todo-app.spec.ts b/apps/web/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 28a8669dd86..00000000000 --- a/apps/web/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = ['buy some cheese', 'feed the cat', 'book a doctors appointment']; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0]]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count'); - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect( - todoItem.locator('label', { - hasText: TODO_ITEMS[1], - }) - ).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count'); - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction((e) => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction((e) => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction((t) => { - return JSON.parse(localStorage['react-todos']) - .map((todo: any) => todo.title) - .includes(t); - }, title); -} diff --git a/apps/web/tests/integrations-list-page.spec.ts b/apps/web/tests/integrations-list-page.spec.ts index c77de8fb242..a1df8e5d774 100644 --- a/apps/web/tests/integrations-list-page.spec.ts +++ b/apps/web/tests/integrations-list-page.spec.ts @@ -12,7 +12,7 @@ import { import { test, expect } from '@playwright/test'; import { getByTestId, initializeSession, isDarkTheme } from './utils.ts/browser'; -import { checkTableLoading, checkTableRow, clickOnListRow, navigateToGetStarted } from './utils.ts/integrations'; +import { checkTableLoading, checkTableRow, clickOnListRow } from './utils.ts/integrations'; import { deleteProvider } from './utils.ts/plugins'; let session; From bf04198664424a6fb2e47f96c16c924ec186d516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Tymczuk?= Date: Fri, 12 Apr 2024 16:01:07 +0200 Subject: [PATCH 17/33] Merge pull request #5401 from novuhq/feature/onb-exp-v2-modal feat: add modal on get started page for onboarding experiment v2 --- .../web/src/constants/experimentsConstants.ts | 1 + .../auth/components/QuestionnaireForm.tsx | 3 +- .../components/OnboardingExperimentModal.tsx | 125 ++++++++++++++++++ .../pages/quick-start/steps/GetStarted.tsx | 15 ++- .../components/TriggerSnippetTabs.tsx | 29 ++-- .../templates/workflow/WorkflowEditor.tsx | 2 +- 6 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 apps/web/src/constants/experimentsConstants.ts create mode 100644 apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx diff --git a/apps/web/src/constants/experimentsConstants.ts b/apps/web/src/constants/experimentsConstants.ts new file mode 100644 index 00000000000..2720076d019 --- /dev/null +++ b/apps/web/src/constants/experimentsConstants.ts @@ -0,0 +1 @@ +export const OnboardingExperimentV2ModalKey = 'nv_onboarding_modal'; diff --git a/apps/web/src/pages/auth/components/QuestionnaireForm.tsx b/apps/web/src/pages/auth/components/QuestionnaireForm.tsx index 1b9f2404cb7..d698ba91821 100644 --- a/apps/web/src/pages/auth/components/QuestionnaireForm.tsx +++ b/apps/web/src/pages/auth/components/QuestionnaireForm.tsx @@ -26,6 +26,7 @@ import { useVercelIntegration, useVercelParams } from '../../../hooks'; import { ROUTES } from '../../../constants/routes.enum'; import { DynamicCheckBox } from './dynamic-checkbox/DynamicCheckBox'; import styled from '@emotion/styled/macro'; +import { OnboardingExperimentV2ModalKey } from '../../../constants/experimentsConstants'; export function QuestionnaireForm() { const [loading, setLoading] = useState(); @@ -66,7 +67,7 @@ export function QuestionnaireForm() { const createDto: ICreateOrganizationDto = { ...rest, name: organizationName }; const organization = await createOrganizationMutation(createDto); const organizationResponseToken = await api.post(`/v1/auth/organizations/${organization._id}/switch`, {}); - + localStorage.setItem(OnboardingExperimentV2ModalKey, 'true'); setToken(organizationResponseToken); } diff --git a/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx b/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx new file mode 100644 index 00000000000..4aaf6e36e00 --- /dev/null +++ b/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx @@ -0,0 +1,125 @@ +import { useState } from 'react'; +import { Modal, useMantineTheme, Grid } from '@mantine/core'; + +import styled from '@emotion/styled'; +import { colors, shadows, Title, Button } from '@novu/design-system'; +import { useAuthContext, useSegment } from '@novu/shared-web'; +import { useCreateOnboardingExperimentWorkflow } from '../../../api/hooks/notification-templates/useCreateOnboardingExperimentWorkflow'; +import { OnboardingExperimentV2ModalKey } from '../../../constants/experimentsConstants'; +import { OnBoardingAnalyticsEnum } from '../consts'; + +export function OnboardingExperimentModal() { + const [opened, setOpened] = useState(true); + const theme = useMantineTheme(); + const segment = useSegment(); + const { currentOrganization } = useAuthContext(); + const { + createOnboardingExperimentWorkflow, + isLoading: IsCreateOnboardingExpWorkflowLoading, + isDisabled: isIsCreateOnboardingExpWorkflowDisabled, + } = useCreateOnboardingExperimentWorkflow(); + const handleOnClose = () => { + setOpened(true); + }; + + return ( + What would you like to do first?} + sx={{ backdropFilter: 'blur(10px)' }} + shadow={theme.colorScheme === 'dark' ? shadows.dark : shadows.medium} + radius="md" + size="lg" + onClose={handleOnClose} + centered + withCloseButton={false} + > + + + + Send test notification + Learn how to setup a workflow and send your first email notification. + { + segment.track(OnBoardingAnalyticsEnum.ONBOARDING_EXPERIMENT_TEST_NOTIFICATION, { + action: 'Modal - Send test notification', + experiment_id: '2024-w15-onb', + _organization: currentOrganization?._id, + }); + localStorage.removeItem(OnboardingExperimentV2ModalKey); + createOnboardingExperimentWorkflow(); + }} + > + Send test notification now + + + + + + Look around + Start exploring the Novu app on your own terms + { + segment.track(OnBoardingAnalyticsEnum.ONBOARDING_EXPERIMENT_TEST_NOTIFICATION, { + action: 'Modal - Get started', + experiment_id: '2024-w15-onb', + _organization: currentOrganization?._id, + }); + localStorage.removeItem(OnboardingExperimentV2ModalKey); + setOpened(false); + }} + > + Get started + + + + + + ); +} + +const ChannelCard = styled.div` + display: flex; + justify-content: space-between; + flex-direction: column; + max-width: 230px; +`; + +const TitleRow = styled.div` + display: flex; + align-items: center; + font-size: 20px; + line-height: 32px; + margin-bottom: 8px; +`; + +const Description = styled.div` + flex: auto; + font-size: 16px; + line-height: 20px; + margin-bottom: 20px; + color: ${colors.B60}; + height: 60px; +`; + +const StyledButton = styled(Button)` + width: fit-content; + outline: none; +`; diff --git a/apps/web/src/pages/quick-start/steps/GetStarted.tsx b/apps/web/src/pages/quick-start/steps/GetStarted.tsx index 74618334d5c..4a01f78f9b0 100644 --- a/apps/web/src/pages/quick-start/steps/GetStarted.tsx +++ b/apps/web/src/pages/quick-start/steps/GetStarted.tsx @@ -9,6 +9,9 @@ import { ChannelsConfiguration } from '../components/ChannelsConfiguration'; import { GetStartedLayout } from '../components/layout/GetStartedLayout'; import { NavButton } from '../components/NavButton'; import { getStartedSteps, OnBoardingAnalyticsEnum } from '../consts'; +import { OnboardingExperimentModal } from '../components/OnboardingExperimentModal'; +import { useAuthContext } from '@novu/shared-web'; +import { OnboardingExperimentV2ModalKey } from '../../../constants/experimentsConstants'; const ChannelsConfigurationHolder = styled.div` display: flex; @@ -25,16 +28,25 @@ const ChannelsConfigurationHolder = styled.div` export function GetStarted() { const segment = useSegment(); + const { currentOrganization } = useAuthContext(); const [clickedChannel, setClickedChannel] = useState<{ open: boolean; channelType?: ChannelTypeEnum; }>({ open: false }); + const isOnboardingModalEnabled = localStorage.getItem(OnboardingExperimentV2ModalKey) === 'true'; + const onIntegrationModalClose = () => setClickedChannel({ open: false }); useEffect(() => { segment.track(OnBoardingAnalyticsEnum.CONFIGURE_PROVIDER_VISIT); - }, [segment]); + if (isOnboardingModalEnabled) { + segment.track('Welcome modal open - [Onboarding]', { + experiment_id: '2024-w15-onb', + _organization: currentOrganization?._id, + }); + } + }, [currentOrganization?._id, isOnboardingModalEnabled, segment]); function handleOnClick() { segment.track(OnBoardingAnalyticsEnum.CONFIGURE_PROVIDER_NAVIGATION_NEXT_PAGE_CLICK); @@ -60,6 +72,7 @@ export function GetStarted() { /> + {isOnboardingModalEnabled && } ); } diff --git a/apps/web/src/pages/templates/components/TriggerSnippetTabs.tsx b/apps/web/src/pages/templates/components/TriggerSnippetTabs.tsx index 19c7e846d5a..e7d31ec13f7 100644 --- a/apps/web/src/pages/templates/components/TriggerSnippetTabs.tsx +++ b/apps/web/src/pages/templates/components/TriggerSnippetTabs.tsx @@ -66,8 +66,7 @@ novu.trigger('${identifier}', ${JSON.stringify( 2 ) .replace(/"([^"]+)":/g, '$1:') - .replace(/"/g, "'") - .replaceAll('\n', '\n ')}); + .replace(/"/g, "'")}); `; return ( @@ -85,19 +84,19 @@ export const getCurlTriggerSnippet = ( snippet?: Record ) => { const curlSnippet = `curl --location --request POST '${API_ROOT}/v1/events/trigger' \\ - --header 'Authorization: ApiKey ' \\ - --header 'Content-Type: application/json' \\ - --data-raw '${JSON.stringify( - { - name: identifier, - to, - payload, - overrides, - ...snippet, - }, - null, - 2 - ).replaceAll('\n', '\n ')}' +--header 'Authorization: ApiKey ' \\ +--header 'Content-Type: application/json' \\ +--data-raw '${JSON.stringify( + { + name: identifier, + to, + payload, + overrides, + ...snippet, + }, + null, + 2 + )}' `; return ( diff --git a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx index fcd04da28a3..61b3ac9c551 100644 --- a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx +++ b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx @@ -293,7 +293,7 @@ const WorkflowEditor = () => { }} data-test-id="get-snippet-btn" > - {tagsIncludesOnboarding ? 'Test Notification Now' : 'Get Snippet'} + Trigger Notification From 15e7917f8919fbe335c36921e5a261a40f93239a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Fri, 12 Apr 2024 17:09:19 +0200 Subject: [PATCH 18/33] chore(web): fixed description in the onboarding experiment modal --- .../quick-start/components/OnboardingExperimentModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx b/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx index 4aaf6e36e00..e02139bcea6 100644 --- a/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx +++ b/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx @@ -49,8 +49,8 @@ export function OnboardingExperimentModal() { - Send test notification - Learn how to setup a workflow and send your first email notification. + Send test notification + Learn how to set up a workflow and send your first email notification. Date: Fri, 12 Apr 2024 17:31:38 +0200 Subject: [PATCH 19/33] chore(web): skipping tests moved to playwright --- apps/web/cypress/tests/integrations-list-page.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/cypress/tests/integrations-list-page.spec.ts b/apps/web/cypress/tests/integrations-list-page.spec.ts index 7e5c8e7963b..57a31fa54bb 100644 --- a/apps/web/cypress/tests/integrations-list-page.spec.ts +++ b/apps/web/cypress/tests/integrations-list-page.spec.ts @@ -22,7 +22,7 @@ Cypress.on('window:before:load', (win) => { * The tests from this file were moved to the corresponding Playwright file apps/web/tests/integrations-list-page.spec.ts. * @deprecated */ -describe('Integrations List Page', function () { +describe.skip('Integrations List Page', function () { let session: any; beforeEach(function () { From e117b80ac8426d746080c0e88fc0f6c606e672e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Fri, 12 Apr 2024 21:54:34 +0200 Subject: [PATCH 20/33] chore(ci): trying to fix the e2e tests web pipeline --- .github/workflows/reusable-web-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-web-e2e.yml b/.github/workflows/reusable-web-e2e.yml index c5c7bdae22e..e18c084ae1f 100644 --- a/.github/workflows/reusable-web-e2e.yml +++ b/.github/workflows/reusable-web-e2e.yml @@ -142,7 +142,7 @@ jobs: spec: "cypress/tests/**/*.spec.{js,jsx,ts,tsx}" - name: Install Playwright Browsers - run: npx playwright install --with-deps + run: pnpm dlx playwright install --with-deps - name: Run Playwright tests working-directory: apps/web From b4f9c622bbb5646dfb35a55b6c09c369e3c4b4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Fri, 12 Apr 2024 23:03:45 +0200 Subject: [PATCH 21/33] chore(ci): playwright config --- .github/workflows/reusable-web-e2e.yml | 74 +++++++++++++------------- apps/web/playwright.config.ts | 22 ++++---- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/.github/workflows/reusable-web-e2e.yml b/.github/workflows/reusable-web-e2e.yml index e18c084ae1f..710e14e151c 100644 --- a/.github/workflows/reusable-web-e2e.yml +++ b/.github/workflows/reusable-web-e2e.yml @@ -104,45 +104,45 @@ jobs: - run: | echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV - - name: Cypress run EE e2e - if: ${{ steps.setup.outputs.has_token == 'true' && inputs.ee }} - uses: cypress-io/github-action@v6 - env: - NODE_ENV: test - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} - CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} - CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} - CYPRESS_IS_CI: true - with: - working-directory: apps/web - browser: "${{ env.BROWSER_PATH }}" - record: false - install: false - parallel: false - config-file: cypress.config.ts - spec: "cypress/tests/**/*.spec-ee.{js,jsx,ts,tsx}" - - - name: Cypress run e2e - uses: cypress-io/github-action@v6 - env: - NODE_ENV: test - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} - CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} - CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} - CYPRESS_IS_CI: true - with: - working-directory: apps/web - browser: "${{ env.BROWSER_PATH }}" - record: false - parallel: false - install: false - config-file: cypress.config.ts - spec: "cypress/tests/**/*.spec.{js,jsx,ts,tsx}" + # - name: Cypress run EE e2e + # if: ${{ steps.setup.outputs.has_token == 'true' && inputs.ee }} + # uses: cypress-io/github-action@v6 + # env: + # NODE_ENV: test + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} + # CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} + # CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} + # CYPRESS_IS_CI: true + # with: + # working-directory: apps/web + # browser: "${{ env.BROWSER_PATH }}" + # record: false + # install: false + # parallel: true + # config-file: cypress.config.ts + # spec: "cypress/tests/**/*.spec-ee.{js,jsx,ts,tsx}" + + # - name: Cypress run e2e + # uses: cypress-io/github-action@v6 + # env: + # NODE_ENV: test + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} + # CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} + # CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} + # CYPRESS_IS_CI: true + # with: + # working-directory: apps/web + # browser: "${{ env.BROWSER_PATH }}" + # record: false + # parallel: true + # install: false + # config-file: cypress.config.ts + # spec: "cypress/tests/**/*.spec.{js,jsx,ts,tsx}" - name: Install Playwright Browsers - run: pnpm dlx playwright install --with-deps + run: pnpm exec playwright install --with-deps - name: Run Playwright tests working-directory: apps/web diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 0ee7fe32eca..d1f00f621da 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -40,16 +40,16 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + /* + * { + * name: 'firefox', + * use: { ...devices['Desktop Firefox'] }, + * }, + * { + * name: 'webkit', + * use: { ...devices['Desktop Safari'] }, + * }, + */ /* Test against mobile viewports. */ /* From 2fe3347b3e85b93a84e3a8c9308c6970f643555b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Fri, 12 Apr 2024 23:29:15 +0200 Subject: [PATCH 22/33] chore(ci): fix playwright pipeline --- .github/workflows/reusable-web-e2e.yml | 79 +++++++++++++------------- apps/web/package.json | 3 +- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/.github/workflows/reusable-web-e2e.yml b/.github/workflows/reusable-web-e2e.yml index 710e14e151c..2b1f078c5c1 100644 --- a/.github/workflows/reusable-web-e2e.yml +++ b/.github/workflows/reusable-web-e2e.yml @@ -104,45 +104,46 @@ jobs: - run: | echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV - # - name: Cypress run EE e2e - # if: ${{ steps.setup.outputs.has_token == 'true' && inputs.ee }} - # uses: cypress-io/github-action@v6 - # env: - # NODE_ENV: test - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} - # CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} - # CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} - # CYPRESS_IS_CI: true - # with: - # working-directory: apps/web - # browser: "${{ env.BROWSER_PATH }}" - # record: false - # install: false - # parallel: true - # config-file: cypress.config.ts - # spec: "cypress/tests/**/*.spec-ee.{js,jsx,ts,tsx}" - - # - name: Cypress run e2e - # uses: cypress-io/github-action@v6 - # env: - # NODE_ENV: test - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} - # CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} - # CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} - # CYPRESS_IS_CI: true - # with: - # working-directory: apps/web - # browser: "${{ env.BROWSER_PATH }}" - # record: false - # parallel: true - # install: false - # config-file: cypress.config.ts - # spec: "cypress/tests/**/*.spec.{js,jsx,ts,tsx}" - - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps + - name: Cypress run EE e2e + if: ${{ steps.setup.outputs.has_token == 'true' && inputs.ee }} + uses: cypress-io/github-action@v6 + env: + NODE_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} + CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} + CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} + CYPRESS_IS_CI: true + with: + working-directory: apps/web + browser: "${{ env.BROWSER_PATH }}" + record: false + install: false + parallel: true + config-file: cypress.config.ts + spec: "cypress/tests/**/*.spec-ee.{js,jsx,ts,tsx}" + + - name: Cypress run e2e + uses: cypress-io/github-action@v6 + env: + NODE_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} + CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} + CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} + CYPRESS_IS_CI: true + with: + working-directory: apps/web + browser: "${{ env.BROWSER_PATH }}" + record: false + parallel: true + install: false + config-file: cypress.config.ts + spec: "cypress/tests/**/*.spec.{js,jsx,ts,tsx}" + + - name: Playwright Install + working-directory: apps/web + run: pnpm playwright:install - name: Run Playwright tests working-directory: apps/web diff --git a/apps/web/package.json b/apps/web/package.json index 73d7b56f002..6b4725ca388 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,7 @@ "version": "0.24.1", "private": true, "scripts": { - "start": "pnpm panda --watch & cross-env PORT=4200 react-app-rewired start", + "start": "pnpm panda --watch & cross-env NODE_OPTIONS=--max_old_space_size=8192 PORT=4200 react-app-rewired start", "prebuild": "rimraf build", "build": "pnpm panda && cross-env NODE_OPTIONS=--max_old_space_size=4096 GENERATE_SOURCEMAP=false react-app-rewired --max_old_space_size=4096 build", "precommit": "lint-staged", @@ -19,6 +19,7 @@ "cypress:install": "cypress install", "cypress:open": "cross-env NODE_ENV=test cypress open", "cypress:run:components": "cross-env NODE_OPTIONS=--max_old_space_size=4096 NODE_ENV=test cypress run --component", + "playwright:install": "playwright install --with-deps", "playwright:test": "playwright test", "playwright:test-ui": "playwright test --ui", "playwright:codegen": "playwright codegen", From 6dfd9eca43d0a62a54483c733f56aa5911ffb601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Sun, 14 Apr 2024 20:57:58 +0200 Subject: [PATCH 23/33] chore(web): migrated activity page tests to playwright --- apps/web/.env.playwirght.test | 1 + apps/web/cypress/tests/activity-graph.spec.ts | 6 ++- apps/web/tests/activity-graph.spec.ts | 25 +++++++++ apps/web/tests/utils.ts/plugins.ts | 53 ++++++++++++++++++- 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 apps/web/tests/activity-graph.spec.ts diff --git a/apps/web/.env.playwirght.test b/apps/web/.env.playwirght.test index d5d0a22a1f1..ba5e5ef43c5 100644 --- a/apps/web/.env.playwirght.test +++ b/apps/web/.env.playwirght.test @@ -1,2 +1,3 @@ NODE_ENV=test MONGODB_URL=mongodb://127.0.0.1:27017/novu-test +API_URL=http://127.0.0.1:1336 diff --git a/apps/web/cypress/tests/activity-graph.spec.ts b/apps/web/cypress/tests/activity-graph.spec.ts index 09ac09f78f5..e9a2aa20d70 100644 --- a/apps/web/cypress/tests/activity-graph.spec.ts +++ b/apps/web/cypress/tests/activity-graph.spec.ts @@ -1,4 +1,8 @@ -describe('Activity page', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/activity-graph.spec.ts. + * @deprecated + */ +describe.skip('Activity page', function () { beforeEach(function () { // @ts-expect-error cy.initializeSession() diff --git a/apps/web/tests/activity-graph.spec.ts b/apps/web/tests/activity-graph.spec.ts new file mode 100644 index 00000000000..f8b784ce627 --- /dev/null +++ b/apps/web/tests/activity-graph.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; + +import { getByTestId, initializeSession } from './utils.ts/browser'; +import { createNotifications } from './utils.ts/plugins'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context); + await createNotifications({ + identifier: session.templates[0].triggers[0].identifier, + token: session.token, + count: 25, + organizationId: session.organization._id, + environmentId: session.environment._id, + }); +}); + +test('should be able to add a new channel', async ({ page }) => { + await page.goto('/activities'); + await expect(page).toHaveURL(/\/activities/); + + const addChannelButton = getByTestId(page, 'activity-stats-weekly-sent'); + await expect(addChannelButton).toContainText('25'); +}); diff --git a/apps/web/tests/utils.ts/plugins.ts b/apps/web/tests/utils.ts/plugins.ts index 513e34f4f44..013a6dfaa42 100644 --- a/apps/web/tests/utils.ts/plugins.ts +++ b/apps/web/tests/utils.ts/plugins.ts @@ -1,6 +1,12 @@ import { DalService, IntegrationRepository, NotificationTemplateEntity } from '@novu/dal'; import { ChannelTypeEnum, ProvidersIdEnum } from '@novu/shared'; -import { UserSession, NotificationTemplateService } from '@novu/testing'; +import { + UserSession, + NotificationTemplateService, + SubscribersService, + NotificationsService, + JobsService, +} from '@novu/testing'; export interface ISessionOptions { noEnvironment?: boolean; @@ -69,3 +75,48 @@ export async function deleteProvider(query: { _organizationId: query.organizationId, }); } + +export async function createNotifications({ + identifier, + token, + count = 1, + subscriberId, + environmentId, + organizationId, + templateId, +}: { + identifier: string; + token: string; + count?: number; + subscriberId?: string; + environmentId: string; + organizationId: string; + templateId?: string; +}) { + const jobsService = new JobsService(); + let subId = subscriberId; + if (!subId) { + const subscribersService = new SubscribersService(organizationId, environmentId); + const subscriber = await subscribersService.createSubscriber(); + subId = subscriber.subscriberId; + } + + const triggerIdentifier = identifier; + const service = new NotificationsService(token); + const session = new UserSession(process.env.API_URL); + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < count; i++) { + await service.triggerEvent(triggerIdentifier, subId, {}); + } + + if (organizationId) { + await session.awaitRunningJobs(templateId, undefined, 0, organizationId); + } + + while ((await jobsService.standardQueue.getWaitingCount()) || (await jobsService.standardQueue.getActiveCount())) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + + return 'ok'; +} From 2837079f646270a202fc4c0278c34232ef0fa0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Sun, 14 Apr 2024 21:30:43 +0200 Subject: [PATCH 24/33] chore(web): removed playwright ci workflow --- apps/web/.github/workflows/playwright.yml | 27 ----------------------- 1 file changed, 27 deletions(-) delete mode 100644 apps/web/.github/workflows/playwright.yml diff --git a/apps/web/.github/workflows/playwright.yml b/apps/web/.github/workflows/playwright.yml deleted file mode 100644 index 9662b54e97f..00000000000 --- a/apps/web/.github/workflows/playwright.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install dependencies - run: npm install -g pnpm && pnpm install - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps - - name: Run Playwright tests - run: pnpm exec playwright test - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 From 5e3f78afa0e06756c9131f58c73d5d79088c59d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Sun, 14 Apr 2024 21:54:18 +0200 Subject: [PATCH 25/33] chore(web): migrated start-from-scratch-tour spec file to playwright --- .../tests/start-from-scratch-tour.spec.ts | 6 +- .../web/tests/start-from-scratch-tour.spec.ts | 183 ++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 apps/web/tests/start-from-scratch-tour.spec.ts diff --git a/apps/web/cypress/tests/start-from-scratch-tour.spec.ts b/apps/web/cypress/tests/start-from-scratch-tour.spec.ts index dbcdd76d6a7..ace6acd53fd 100644 --- a/apps/web/cypress/tests/start-from-scratch-tour.spec.ts +++ b/apps/web/cypress/tests/start-from-scratch-tour.spec.ts @@ -1,4 +1,8 @@ -describe('Start from scratch tour hints', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/start-from-scratch-tour.spec.ts. + * @deprecated + */ +describe.skip('Start from scratch tour hints', function () { beforeEach(function () { cy.initializeSession({ showOnBoardingTour: true }).as('session'); }); diff --git a/apps/web/tests/start-from-scratch-tour.spec.ts b/apps/web/tests/start-from-scratch-tour.spec.ts new file mode 100644 index 00000000000..9463676dadb --- /dev/null +++ b/apps/web/tests/start-from-scratch-tour.spec.ts @@ -0,0 +1,183 @@ +import { test, expect } from '@playwright/test'; +import os from 'node:os'; + +import { getByTestId, initializeSession } from './utils.ts/browser'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context, { showOnBoardingTour: true }); +}); + +test('should show the start from scratch intro step', async ({ page }) => { + await page.goto('/workflows/create'); + + const scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); +}); + +test('should hide the start from scratch intro step after clicking on watch later', async ({ page }) => { + await page.goto('/workflows/create'); + + const scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); + + const scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + await scratchWorkflowTooltipSkipButton.click(); + + await expect(scratchWorkflowTooltip).not.toBeVisible(); +}); + +test('should show the start from scratch tour hints', async ({ page }) => { + await page.goto('/workflows/create'); + + const scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); + await scratchWorkflowTooltipPrimaryButton.click(); + + const scratchWorkflowTooltipTitle2 = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle2).toHaveText('Click to edit workflow name'); + + const scratchWorkflowTooltipDescription2 = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription2).toHaveText( + 'Specify a name for your workflow here or in the workflow settings.' + ); + + const scratchWorkflowTooltipPrimaryButton2 = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton2).toHaveText('Next'); + await scratchWorkflowTooltipPrimaryButton2.click(); + + const scratchWorkflowTooltipTitle3 = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle3).toHaveText('Verify workflow settings'); + + const scratchWorkflowTooltipDescription3 = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription3).toHaveText( + 'Manage name, identifier, group and description. Set up channels, active by default.' + ); + + const scratchWorkflowTooltipPrimaryButton3 = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton3).toHaveText('Next'); + await scratchWorkflowTooltipPrimaryButton3.click(); + + const scratchWorkflowTooltipTitle4 = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle4).toHaveText('Build a notification workflow'); + + const scratchWorkflowTooltipDescription4 = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription4).toHaveText( + 'Add channels you would like to send notifications to. The channels will be inserted to the trigger snippet.' + ); + + const scratchWorkflowTooltipPrimaryButton4 = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton4).toHaveText('Next'); + await scratchWorkflowTooltipPrimaryButton4.click(); + + const scratchWorkflowTooltipTitle5 = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle5).toHaveText('Run a test or Get Snippet'); + + const scratchWorkflowTooltipDescription5 = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription5).toHaveText( + 'Test a trigger as if it was sent from your API. Deploy it to your App by copy/paste trigger snippet.' + ); + + const gotItButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(gotItButton).toHaveText('Got it'); + await gotItButton.click(); + + await expect(scratchWorkflowTooltip).not.toBeVisible(); +}); + +test('should show the dots navigation after the intro step', async ({ page }) => { + await page.goto('/workflows/create'); + + const scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); + await scratchWorkflowTooltipPrimaryButton.click(); + + const dotsNavigation = await getByTestId(page, 'scratch-workflow-tooltip-dots-navigation'); + await expect(dotsNavigation).toBeVisible(); +}); + +test('should show not show the start from scratch tour hints after it is shown twice ', async ({ page }) => { + await page.goto('/workflows/create'); + + let scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); + + let scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + await scratchWorkflowTooltipSkipButton.click(); + + await expect(scratchWorkflowTooltip).not.toBeVisible(); + + await page.reload(); + + scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + await scratchWorkflowTooltipSkipButton.click(); + + const scratchWorkflowTooltip2 = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip2).not.toBeVisible(); + + await page.reload(); + + scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).not.toBeVisible(); +}); From af41c6f1ce89d03953370c4f67797e344405d8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Sun, 14 Apr 2024 22:40:10 +0200 Subject: [PATCH 26/33] chore(web): added coderabbit suggestions from the pr #5313 --- .../web/tests/integrations-list-modal.spec.ts | 112 ++++++++---------- apps/web/tests/integrations-list-page.spec.ts | 109 ++++++++--------- apps/web/tests/utils.ts/integrations.ts | 19 +++ 3 files changed, 118 insertions(+), 122 deletions(-) diff --git a/apps/web/tests/integrations-list-modal.spec.ts b/apps/web/tests/integrations-list-modal.spec.ts index fd889d85bd7..2de77a5600e 100644 --- a/apps/web/tests/integrations-list-modal.spec.ts +++ b/apps/web/tests/integrations-list-modal.spec.ts @@ -12,7 +12,13 @@ import { import { test, expect } from '@playwright/test'; import { getByTestId, initializeSession, isDarkTheme } from './utils.ts/browser'; -import { checkTableLoading, checkTableRow, clickOnListRow, navigateToGetStarted } from './utils.ts/integrations'; +import { + checkTableLoading, + checkTableRow, + clickOnListRow, + interceptIntegrationsRequest, + navigateToGetStarted, +} from './utils.ts/integrations'; import { deleteProvider } from './utils.ts/plugins'; let session; @@ -22,14 +28,9 @@ test.beforeEach(async ({ context }) => { }); test('should show the table loading skeleton and empty state', async ({ page }) => { - const integrationsPromise = page.route('**/integrations', async (route) => { - const response = await page.request.fetch(route.request()); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - await route.fulfill({ - response, - body: JSON.stringify({ data: [] }), - }); + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: () => ({ data: [] }), }); await navigateToGetStarted(page, 'channel-card-sms'); @@ -38,7 +39,6 @@ test('should show the table loading skeleton and empty state', async ({ page }) await expect(providerSidebar).toBeVisible(); const sidebarClose = getByTestId(page, 'sidebar-close'); - await expect(sidebarClose).toBeVisible(); await sidebarClose.click(); await checkTableLoading(page); @@ -66,15 +66,8 @@ test('should show the table loading skeleton and empty state', async ({ page }) }); test('should show the table loading skeleton and then table', async ({ page }) => { - const integrationsPromise = page.route('**/integrations', async (route) => { - await new Promise((resolve) => setTimeout(resolve, 2000)); - const response = await page.request.fetch(route.request()); - const body = await response.json(); - - await route.fulfill({ - response, - body: JSON.stringify(body), - }); + const integrationsPromise = interceptIntegrationsRequest({ + page, }); await navigateToGetStarted(page, 'channel-card-sms'); @@ -83,7 +76,6 @@ test('should show the table loading skeleton and then table', async ({ page }) = await expect(providerSidebar).toBeVisible(); const sidebarClose = getByTestId(page, 'sidebar-close'); - await expect(sidebarClose).toBeVisible(); await sidebarClose.click(); await checkTableLoading(page); @@ -715,27 +707,25 @@ test('should show the Novu in-app integration - React guide', async ({ page }) = }); test('should show the Novu Email integration sidebar', async ({ page }) => { - const integrationsPromise = page.route('**/integrations', async (route) => { - const response = await page.request.fetch(route.request()); - const body = await response.json(); - const [firstIntegration] = body.data; - body.data = [ - { - _id: EmailProviderIdEnum.Novu, - _environmentId: firstIntegration._environmentId, - providerId: EmailProviderIdEnum.Novu, - active: true, - channel: ChannelTypeEnum.EMAIL, - name: 'Novu Email', - identifier: EmailProviderIdEnum.Novu, - }, - ...body.data, - ]; - - await route.fulfill({ - response, - body: JSON.stringify(body), - }); + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: (body) => { + const [firstIntegration] = body.data; + body.data = [ + { + _id: EmailProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: EmailProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.EMAIL, + name: 'Novu Email', + identifier: EmailProviderIdEnum.Novu, + }, + ...body.data, + ]; + + return body; + }, }); await navigateToGetStarted(page); @@ -780,27 +770,25 @@ test('should show the Novu Email integration sidebar', async ({ page }) => { }); test('should show the Novu SMS integration sidebar', async ({ page }) => { - const integrationsPromise = page.route('**/integrations', async (route) => { - const response = await page.request.fetch(route.request()); - const body = await response.json(); - const [firstIntegration] = body.data; - body.data = [ - { - _id: SmsProviderIdEnum.Novu, - _environmentId: firstIntegration._environmentId, - providerId: SmsProviderIdEnum.Novu, - active: true, - channel: ChannelTypeEnum.SMS, - name: 'Novu SMS', - identifier: SmsProviderIdEnum.Novu, - }, - ...body.data, - ]; - - await route.fulfill({ - response, - body: JSON.stringify(body), - }); + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: (body) => { + const [firstIntegration] = body.data; + body.data = [ + { + _id: SmsProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: SmsProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.SMS, + name: 'Novu SMS', + identifier: SmsProviderIdEnum.Novu, + }, + ...body.data, + ]; + + return body; + }, }); await navigateToGetStarted(page); diff --git a/apps/web/tests/integrations-list-page.spec.ts b/apps/web/tests/integrations-list-page.spec.ts index a1df8e5d774..72f0231e7c1 100644 --- a/apps/web/tests/integrations-list-page.spec.ts +++ b/apps/web/tests/integrations-list-page.spec.ts @@ -12,7 +12,12 @@ import { import { test, expect } from '@playwright/test'; import { getByTestId, initializeSession, isDarkTheme } from './utils.ts/browser'; -import { checkTableLoading, checkTableRow, clickOnListRow } from './utils.ts/integrations'; +import { + checkTableLoading, + checkTableRow, + clickOnListRow, + interceptIntegrationsRequest, +} from './utils.ts/integrations'; import { deleteProvider } from './utils.ts/plugins'; let session; @@ -22,14 +27,9 @@ test.beforeEach(async ({ context }) => { }); test('should show the table loading skeleton and empty state', async ({ page }) => { - const integrationsPromise = page.route('**/v1/integrations', async (route) => { - const response = await page.request.fetch(route.request()); - await new Promise((resolve) => setTimeout(resolve, 2000)); - - await route.fulfill({ - response, - body: JSON.stringify({ data: [] }), - }); + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: () => ({ data: [] }), }); await page.goto('/integrations'); @@ -60,15 +60,8 @@ test('should show the table loading skeleton and empty state', async ({ page }) }); test('should show the table loading skeleton and then table', async ({ page }) => { - const integrationsPromise = page.route('**/v1/integrations', async (route) => { - await new Promise((resolve) => setTimeout(resolve, 2000)); - const response = await page.request.fetch(route.request()); - const body = await response.json(); - - await route.fulfill({ - response, - body: JSON.stringify(body), - }); + const integrationsPromise = interceptIntegrationsRequest({ + page, }); await page.goto('/integrations'); @@ -992,27 +985,25 @@ test('should show the Novu in-app integration - React guide', async ({ page }) = }); test('should show the Novu Email integration sidebar', async ({ page }) => { - const integrationsPromise = page.route('**/v1/integrations', async (route) => { - const response = await page.request.fetch(route.request()); - const body = await response.json(); - const [firstIntegration] = body.data; - body.data = [ - { - _id: EmailProviderIdEnum.Novu, - _environmentId: firstIntegration._environmentId, - providerId: EmailProviderIdEnum.Novu, - active: true, - channel: ChannelTypeEnum.EMAIL, - name: 'Novu Email', - identifier: EmailProviderIdEnum.Novu, - }, - ...body.data, - ]; - - await route.fulfill({ - response, - body: JSON.stringify(body), - }); + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: (body) => { + const [firstIntegration] = body.data; + body.data = [ + { + _id: EmailProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: EmailProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.EMAIL, + name: 'Novu Email', + identifier: EmailProviderIdEnum.Novu, + }, + ...body.data, + ]; + + return body; + }, }); await page.goto('/integrations'); @@ -1055,27 +1046,25 @@ test('should show the Novu Email integration sidebar', async ({ page }) => { }); test('should show the Novu SMS integration sidebar', async ({ page }) => { - const integrationsPromise = page.route('**/v1/integrations', async (route) => { - const response = await page.request.fetch(route.request()); - const body = await response.json(); - const [firstIntegration] = body.data; - body.data = [ - { - _id: SmsProviderIdEnum.Novu, - _environmentId: firstIntegration._environmentId, - providerId: SmsProviderIdEnum.Novu, - active: true, - channel: ChannelTypeEnum.SMS, - name: 'Novu SMS', - identifier: SmsProviderIdEnum.Novu, - }, - ...body.data, - ]; - - await route.fulfill({ - response, - body: JSON.stringify(body), - }); + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: (body) => { + const [firstIntegration] = body.data; + body.data = [ + { + _id: SmsProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: SmsProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.SMS, + name: 'Novu SMS', + identifier: SmsProviderIdEnum.Novu, + }, + ...body.data, + ]; + + return body; + }, }); await page.goto('/integrations'); diff --git a/apps/web/tests/utils.ts/integrations.ts b/apps/web/tests/utils.ts/integrations.ts index d4efab64896..f16ed59cfb5 100644 --- a/apps/web/tests/utils.ts/integrations.ts +++ b/apps/web/tests/utils.ts/integrations.ts @@ -86,3 +86,22 @@ export const clickOnListRow = async (page: Page | Locator, name: string | RegExp await expect(row).toBeVisible(); await row.click(); }; + +export async function interceptIntegrationsRequest({ + page, + modifyBody, +}: { + page: Page; + modifyBody?: (body: any) => any; +}) { + return page.route('**/v1/integrations', async (route) => { + const response = await page.request.fetch(route.request()); + await new Promise((resolve) => setTimeout(resolve, 3000)); + const body = await response.json(); + + await route.fulfill({ + response, + body: JSON.stringify(modifyBody ? modifyBody(body) : body), + }); + }); +} From 58f8ecf4a0b01e20250dcccaa5006e61924f79e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dziuba?= Date: Mon, 15 Apr 2024 14:30:28 +0200 Subject: [PATCH 27/33] feat: add comparison operators helpers (#5334) Co-authored-by: Dima Grossman --- .../handlebar-helpers/handlebarHelpers.ts | 12 ++ .../compile-template/compile-template.spec.ts | 144 ++++++++++++++++++ .../compile-template.usecase.ts | 54 +++++++ 3 files changed, 210 insertions(+) diff --git a/libs/shared/src/consts/handlebar-helpers/handlebarHelpers.ts b/libs/shared/src/consts/handlebar-helpers/handlebarHelpers.ts index 5595ffb44bf..99e855a48df 100644 --- a/libs/shared/src/consts/handlebar-helpers/handlebarHelpers.ts +++ b/libs/shared/src/consts/handlebar-helpers/handlebarHelpers.ts @@ -10,6 +10,12 @@ export enum HandlebarHelpersEnum { SORT_BY = 'sortBy', NUMBERFORMAT = 'numberFormat', I18N = 'i18n', + GT = 'gt', + GTE = 'gte', + LT = 'lt', + LTE = 'lte', + EQ = 'eq', + NE = 'ne', } // eslint-disable-next-line @typescript-eslint/naming-convention @@ -25,4 +31,10 @@ export const HandlebarHelpers = { [HandlebarHelpersEnum.SORT_BY]: { description: 'sort an array of objects by a property' }, [HandlebarHelpersEnum.NUMBERFORMAT]: { description: 'format number' }, [HandlebarHelpersEnum.I18N]: { description: 'translate' }, + [HandlebarHelpersEnum.GT]: { description: 'greater than' }, + [HandlebarHelpersEnum.GTE]: { description: 'greater than or equal to' }, + [HandlebarHelpersEnum.LT]: { description: 'lesser than' }, + [HandlebarHelpersEnum.LTE]: { description: 'lesser than or equal to' }, + [HandlebarHelpersEnum.EQ]: { description: 'strict equal' }, + [HandlebarHelpersEnum.NE]: { description: 'strict not equal to' }, }; diff --git a/packages/application-generic/src/usecases/compile-template/compile-template.spec.ts b/packages/application-generic/src/usecases/compile-template/compile-template.spec.ts index 4003ac48f29..9b2393168b2 100644 --- a/packages/application-generic/src/usecases/compile-template/compile-template.spec.ts +++ b/packages/application-generic/src/usecases/compile-template/compile-template.spec.ts @@ -209,4 +209,148 @@ describe('Compile Template', function () { expect(result).toEqual('
Not a number
'); }); }); + + describe('gt helper', () => { + const template = `{{#gt steps 5 }}gt block{{else}}else block{{/gt}}`; + it('shoud render gt block', async () => { + const result = await useCase.execute({ + data: { steps: 6 }, + template, + }); + + expect(result).toEqual('gt block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('gte helper', () => { + const template = `{{#gte steps 5 }}gte block{{else}}else block{{/gte}}`; + it('shoud render gte block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('gte block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 4 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('lt helper', () => { + const template = `{{#lt steps 5 }}lt block{{else}}else block{{/lt}}`; + it('shoud render lt block', async () => { + const result = await useCase.execute({ + data: { steps: 4 }, + template, + }); + + expect(result).toEqual('lt block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('lte helper', () => { + const template = `{{#lte steps 5 }}lte block{{else}}else block{{/lte}}`; + it('shoud render lte block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('lte block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 6 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('eq helper', () => { + const template = `{{#eq steps 5 }}eq block{{else}}else block{{/eq}}`; + it('shoud render eq block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('eq block'); + }); + + it('shoud use strict check and render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: '5' }, + template, + }); + + expect(result).toEqual('else block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 6 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('ne helper', () => { + const template = `{{#ne steps 5 }}ne block{{else}}else block{{/ne}}`; + it('shoud render ne block', async () => { + const result = await useCase.execute({ + data: { steps: 6 }, + template, + }); + + expect(result).toEqual('ne block'); + }); + + it('shoud use strict check and render ne block', async () => { + const result = await useCase.execute({ + data: { steps: '5' }, + template, + }); + + expect(result).toEqual('ne block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); }); diff --git a/packages/application-generic/src/usecases/compile-template/compile-template.usecase.ts b/packages/application-generic/src/usecases/compile-template/compile-template.usecase.ts index 1669e6c0d13..bd78462aa2c 100644 --- a/packages/application-generic/src/usecases/compile-template/compile-template.usecase.ts +++ b/packages/application-generic/src/usecases/compile-template/compile-template.usecase.ts @@ -158,6 +158,60 @@ Handlebars.registerHelper( } ); +Handlebars.registerHelper( + HandlebarHelpersEnum.GT, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 > arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.GTE, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 >= arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.LT, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 < arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.LTE, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 <= arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.EQ, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 === arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.NE, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 !== arg2 ? options.fn(this) : options.inverse(this); + } +); + @Injectable() export class CompileTemplate { async execute(command: CompileTemplateCommand): Promise { From dd2633165233479a0af6692b210b825f7b2cdb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Mon, 15 Apr 2024 15:32:06 +0200 Subject: [PATCH 28/33] chore(web): update some tests flakiness and running tests in parallel --- apps/web/playwright.config.ts | 2 +- apps/web/tests/integrations-list-modal.spec.ts | 2 +- apps/web/tests/integrations-list-page.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index f16b04b3427..c6872da1b35 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 4 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'blob' : 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/apps/web/tests/integrations-list-modal.spec.ts b/apps/web/tests/integrations-list-modal.spec.ts index 2de77a5600e..9db0e4e0d58 100644 --- a/apps/web/tests/integrations-list-modal.spec.ts +++ b/apps/web/tests/integrations-list-modal.spec.ts @@ -630,7 +630,7 @@ test('should show the Novu in-app integration', async ({ page }) => { 'Select a framework to set up credentials to start sending notifications.' ); - const sidebarClose = getByTestId(page, 'sidebar-close'); + const sidebarClose = getByTestId(updateProviderSidebar, 'sidebar-close'); await expect(sidebarClose).toBeVisible(); const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); diff --git a/apps/web/tests/integrations-list-page.spec.ts b/apps/web/tests/integrations-list-page.spec.ts index 72f0231e7c1..88c1056c81f 100644 --- a/apps/web/tests/integrations-list-page.spec.ts +++ b/apps/web/tests/integrations-list-page.spec.ts @@ -907,7 +907,7 @@ test('should show the Novu in-app integration', async ({ page }) => { 'Select a framework to set up credentials to start sending notifications.' ); - const sidebarClose = getByTestId(page, 'sidebar-close'); + const sidebarClose = getByTestId(updateProviderSidebar, 'sidebar-close'); await expect(sidebarClose).toBeVisible(); const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); From 5b0da563829a512902ec3c8b684a5ec92c36d653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Tymczuk?= Date: Fri, 5 Apr 2024 23:18:13 +0200 Subject: [PATCH 29/33] Merge pull request #5371 from novuhq/nv-3634-fix-nc-scroll fix(notification-center,widget): infinite scroll issue --- apps/widget/cypress/e2e/notifications-list.spec.ts | 5 +++-- apps/widget/cypress/plugins/index.ts | 4 ++++ .../notification-center/components/NotificationsList.tsx | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/widget/cypress/e2e/notifications-list.spec.ts b/apps/widget/cypress/e2e/notifications-list.spec.ts index 37150e8e00a..e7a24666b19 100644 --- a/apps/widget/cypress/e2e/notifications-list.spec.ts +++ b/apps/widget/cypress/e2e/notifications-list.spec.ts @@ -171,11 +171,12 @@ describe('Notifications List', function () { cy.getByTestId('unseen-count-label').should('contain', '99+'); }); - it('pagination check', async function () { + it('pagination check', function () { cy.wait('@getNotificationsFirstPage'); cy.task('createNotifications', { organizationId: this.session.organization._id, enumerate: true, + ordered: true, identifier: this.session.templates[0].triggers[0].identifier, token: this.session.token, subscriberId: this.session.subscriber.subscriberId, @@ -206,5 +207,5 @@ describe('Notifications List', function () { }); function scrollToBottom() { - cy.getByTestId('notifications-scroll-area').get('.infinite-scroll-component').scrollTo('bottom'); + cy.getByTestId('notifications-scroll-area').scrollTo('bottom', { ensureScrollable: true }); } diff --git a/apps/widget/cypress/plugins/index.ts b/apps/widget/cypress/plugins/index.ts index eeb8297abbd..8ca66929785 100644 --- a/apps/widget/cypress/plugins/index.ts +++ b/apps/widget/cypress/plugins/index.ts @@ -33,6 +33,7 @@ module.exports = (on, config) => { count = 1, organizationId, enumerate = false, + ordered = false, }) { const triggerIdentifier = identifier; const service = new NotificationsService(token); @@ -44,6 +45,9 @@ module.exports = (on, config) => { await service.triggerEvent(triggerIdentifier, subscriberId, { firstName: `John${num}`, }); + if (ordered) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } } if (organizationId) { diff --git a/packages/notification-center/src/components/notification-center/components/NotificationsList.tsx b/packages/notification-center/src/components/notification-center/components/NotificationsList.tsx index c84389cb632..bf6b5c08fdd 100644 --- a/packages/notification-center/src/components/notification-center/components/NotificationsList.tsx +++ b/packages/notification-center/src/components/notification-center/components/NotificationsList.tsx @@ -47,4 +47,5 @@ export function NotificationsList({ const notificationsListCss = css` height: 400px; + overflow-y: auto; `; From 377ed3c734da41c95e918b125ca09248aadee072 Mon Sep 17 00:00:00 2001 From: Gosha Date: Mon, 15 Apr 2024 18:04:23 +0300 Subject: [PATCH 30/33] feat: update submodule hash --- .source | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.source b/.source index b6407c754b8..e1cbb901a22 160000 --- a/.source +++ b/.source @@ -1 +1 @@ -Subproject commit b6407c754b85561409db7639eed4242a7b52e62e +Subproject commit e1cbb901a22930b776d797d905cb1b6d2468fe27 From 7bde00e93435b8182260bff3204abfa21444df1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Mon, 15 Apr 2024 17:28:19 +0200 Subject: [PATCH 31/33] chore(ci): remove the api docker image build functionality from the test pipeline --- .github/workflows/test.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86a5862f34c..1bfa4f329a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,30 +108,6 @@ jobs: if: ${{ contains(fromJson(needs.get-affected.outputs.test-cypress), '@novu/widget') || contains(fromJson(needs.get-affected.outputs.test-unit), '@novu/notification-center') || contains(fromJson(needs.get-affected.outputs.test-unit), '@novu/ws') }} secrets: inherit - build_docker_api: - name: Build Docker API - runs-on: ubuntu-latest - timeout-minutes: 80 - needs: [get-affected] - if: ${{ contains(fromJson(needs.get-affected.outputs.test-e2e), '@novu/api') }} - permissions: - contents: read - packages: write - deployments: write - id-token: write - strategy: - matrix: - name: ['novu/api', 'novu/api-ee'] - steps: - - uses: actions/checkout@v3 - - uses: ./.github/actions/setup-project - - uses: ./.github/actions/setup-redis-cluster - - uses: ./.github/actions/docker/build-api - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - fork: ${{ github.event.pull_request.head.repo.fork }} - docker_name: ${{ matrix.name }} - test_providers: name: Unit Test Providers runs-on: ubuntu-latest From 3486a868cf9006aabbf19039fb07406e51d10489 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Tue, 16 Apr 2024 12:24:33 +0300 Subject: [PATCH 32/33] fix(web): Remove strict lastname validation Customers can provide any single word as their full name. We don't benefit from being too strict about it during sign-up as the email address is what matters. --- .../src/pages/auth/components/SignUpForm.tsx | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/apps/web/src/pages/auth/components/SignUpForm.tsx b/apps/web/src/pages/auth/components/SignUpForm.tsx index 5ae9274d156..ca81b0e2b56 100644 --- a/apps/web/src/pages/auth/components/SignUpForm.tsx +++ b/apps/web/src/pages/auth/components/SignUpForm.tsx @@ -3,7 +3,6 @@ import { Link, useNavigate } from 'react-router-dom'; import { useMutation } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { Center } from '@mantine/core'; -import { showNotification } from '@mantine/notifications'; import { passwordConstraints, UTM_CAMPAIGN_QUERY_PARAM } from '@novu/shared'; import type { IResponseError } from '@novu/shared'; import { PasswordInput, Button, colors, Input, Text, Checkbox } from '@novu/design-system'; @@ -13,7 +12,6 @@ import { api } from '../../../api/api.client'; import { applyToken, useVercelParams } from '../../../hooks'; import { useAcceptInvite } from './useAcceptInvite'; import { PasswordRequirementPopover } from './PasswordRequirementPopover'; -import { buildGithubLink, buildVercelGithubLink } from './gitHubUtils'; import { ROUTES } from '../../../constants/routes.enum'; import { OAuth } from './OAuth'; @@ -36,9 +34,6 @@ export function SignUpForm({ invitationToken, email }: SignUpFormProps) { const { isFromVercel, code, next, configurationId } = useVercelParams(); const vercelQueryParams = `code=${code}&next=${next}&configurationId=${configurationId}`; const loginLink = isFromVercel ? `/auth/login?${vercelQueryParams}` : ROUTES.AUTH_LOGIN; - const githubLink = isFromVercel - ? buildVercelGithubLink({ code, next, configurationId }) - : buildGithubLink({ invitationToken }); const { isLoading, mutateAsync, isError, error } = useMutation< { token: string }, @@ -52,21 +47,14 @@ export function SignUpForm({ invitationToken, email }: SignUpFormProps) { >((data) => api.post('/v1/auth/register', data)); const onSubmit = async (data) => { + const [firstName, lastName] = data?.fullName.trim().split(' '); const itemData = { - firstName: data.fullName.split(' ')[0], - lastName: data.fullName.split(' ')[1], + firstName, + lastName, email: data.email, password: data.password, }; - if (!itemData.lastName) { - showNotification({ - message: 'Please write your full name including last name', - color: 'red', - }); - - return; - } const response = await mutateAsync(itemData); /** @@ -80,10 +68,9 @@ export function SignUpForm({ invitationToken, email }: SignUpFormProps) { submitToken(token, invitationToken); return true; - } else { - setToken(token); } + setToken(token); navigate(isFromVercel ? `/auth/application?${vercelQueryParams}` : ROUTES.AUTH_APPLICATION); return true; From ac51cce9dc3f95cd552efc7233c5e57c44d8226f Mon Sep 17 00:00:00 2001 From: Pawan Jain Date: Tue, 16 Apr 2024 15:17:11 +0530 Subject: [PATCH 33/33] fix: add domain parser hook and domain condition (#5413) * fix: add domain parser hook and domain condition * fix: spell check error icann * fix: add validate rule for domain field --- .cspell.json | 1 + .../auth/components/QuestionnaireForm.tsx | 15 +++-- .../pages/auth/components/useDomainHook.ts | 62 +++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/pages/auth/components/useDomainHook.ts diff --git a/.cspell.json b/.cspell.json index 7f3907b461a..5b4cbf3728f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -621,6 +621,7 @@ "Vonage", "runtimes", "cafebabe", + "Icann", "limitbar", ], "flagWords": [], diff --git a/apps/web/src/pages/auth/components/QuestionnaireForm.tsx b/apps/web/src/pages/auth/components/QuestionnaireForm.tsx index d698ba91821..e4068fcaec7 100644 --- a/apps/web/src/pages/auth/components/QuestionnaireForm.tsx +++ b/apps/web/src/pages/auth/components/QuestionnaireForm.tsx @@ -9,7 +9,6 @@ import { JobTitleEnum, jobTitleToLabelMapper, ProductUseCasesEnum } from '@novu/ import type { ProductUseCases, IResponseError, ICreateOrganizationDto, IJwtPayload } from '@novu/shared'; import { Button, - colors, Digest, HalfClock, Input, @@ -27,6 +26,7 @@ import { ROUTES } from '../../../constants/routes.enum'; import { DynamicCheckBox } from './dynamic-checkbox/DynamicCheckBox'; import styled from '@emotion/styled/macro'; import { OnboardingExperimentV2ModalKey } from '../../../constants/experimentsConstants'; +import { useDomainParser } from './useDomainHook'; export function QuestionnaireForm() { const [loading, setLoading] = useState(); @@ -34,11 +34,13 @@ export function QuestionnaireForm() { handleSubmit, formState: { errors }, control, + setError, } = useForm({}); const navigate = useNavigate(); const { setToken, token } = useAuthContext(); const { startVercelSetup } = useVercelIntegration(); const { isFromVercel } = useVercelParams(); + const { parse } = useDomainParser(); const { mutateAsync: createOrganizationMutation } = useMutation< { _id: string }, @@ -162,9 +164,14 @@ export function QuestionnaireForm() { name="domain" control={control} rules={{ - pattern: { - value: /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/, - message: 'Please make sure you specified a valid domain', + validate: { + isValiDomain: (value) => { + const val = parse(value as string); + + if (value && !val.isIcann) { + return 'Please provide a valid domain'; + } + }, }, }} render={({ field }) => { diff --git a/apps/web/src/pages/auth/components/useDomainHook.ts b/apps/web/src/pages/auth/components/useDomainHook.ts new file mode 100644 index 00000000000..68deb7fb0d5 --- /dev/null +++ b/apps/web/src/pages/auth/components/useDomainHook.ts @@ -0,0 +1,62 @@ +import { useCallback, useEffect, useRef } from 'react'; + +interface DomainInfo { + domain: string; + domainWithoutSuffix: string; + hostname: string; + isIcann: boolean; + isIp: boolean; + isPrivate: boolean; + publicSuffix: string; + subdomain: string; +} + +function stripProtocol(url: string): string { + return (url || '').replace(/(https?)?(:\/+)?/, ''); +} + +function fallbackParser(url: string) { + const parts = stripProtocol(url).split('.'); + + if (parts.length > 2) { + const [subdomain, ...domainParts] = parts; + + return { + subdomain: subdomain || '', + domain: domainParts.join('.'), + }; + } + + return { + subdomain: '', + domain: url, + }; +} + +export function useDomainParser(): { parse: (url: string) => Partial } { + const tldParser = useRef(null); + + useEffect(() => { + import( + /* webpackIgnore: true */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line import/extensions + 'https://unpkg.com/tldts/dist/es6/index.js?module' + ) + .then((mod) => (tldParser.current = mod)) + .catch(() => (tldParser.current = null)); + }, []); + + const parse = useCallback((url: string) => { + url = stripProtocol(url); + + if (tldParser.current) { + return (tldParser.current as any).parse(url, { allowPrivateDomains: true }) as DomainInfo; + } + + return fallbackParser(url); + }, []); + + return { parse }; +}