Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(dashboard): user journey smoke tests #7124

Open
wants to merge 28 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e12abe9
chore(dashboard): user journey smoke tests
LetItRock Nov 25, 2024
9f22ad5
chore(dashboard): fix imports in page models
LetItRock Nov 25, 2024
d0a3333
Merge branch 'next' into nv-4795-dashboard-user-journey-smoke-tests
LetItRock Nov 25, 2024
ceabff2
chore(dashboard): remove timeout
LetItRock Nov 25, 2024
8ad2e31
chore(api): revert .env.test
LetItRock Nov 25, 2024
461275c
chore(dashboard): fix spell check and adjust pipelines
LetItRock Nov 25, 2024
241dc38
chore(dashboard): e2e pipeline set env variables earlier
LetItRock Nov 25, 2024
1f5180e
chore(dashboard): fixed e2e tests pipeline env config
LetItRock Nov 25, 2024
ead9157
chore(dashboard): fix the e2e tests pipeline
LetItRock Nov 25, 2024
934a828
chore(dashboard): fix the e2e tests pipeline
LetItRock Nov 25, 2024
7bebc12
chore(dashboard): fix the e2e tests pipeline
LetItRock Nov 26, 2024
a7e8329
chore(dashboard): check env keys
LetItRock Nov 26, 2024
c253764
chore(dashboard): fix env keys
LetItRock Nov 26, 2024
fa860f3
Merge branch 'next' into nv-4795-dashboard-user-journey-smoke-tests
LetItRock Dec 2, 2024
8c8cb67
Merge branch 'next' into nv-4795-dashboard-user-journey-smoke-tests
LetItRock Dec 4, 2024
ba8bf3a
chore(dashboard): updated e2e tests
LetItRock Dec 4, 2024
ebc6c37
chore(dashboard): trying to fix e2e pipeline
LetItRock Dec 4, 2024
2f7b4cf
chore(api): fix used variables
LetItRock Dec 5, 2024
d7f56de
chore(api): fix e2e tests
LetItRock Dec 5, 2024
1aa027a
chore(api): fix e2e tests
LetItRock Dec 5, 2024
addf0fa
chore(api): fix e2e tests
LetItRock Dec 5, 2024
8a70a51
chore(api): fix e2e tests
LetItRock Dec 5, 2024
76c80c0
chore(api): fix e2e tests
LetItRock Dec 5, 2024
a1a5a6b
chore(api): fix e2e tests
LetItRock Dec 5, 2024
9c8f28c
chore(api): fix e2e tests
LetItRock Dec 5, 2024
674861a
Merge branch 'next' into nv-4795-dashboard-user-journey-smoke-tests
LetItRock Dec 5, 2024
5727282
Merge branch 'next' into nv-4795-dashboard-user-journey-smoke-tests
LetItRock Dec 6, 2024
5d0de32
chore(dashboard): polishing the e2e pipeline
LetItRock Dec 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 59 additions & 18 deletions .github/workflows/reusable-dashboard-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,36 @@ jobs:
with:
submodules: true

- name: Create .env file for the Dashboard app
working-directory: apps/dashboard
run: |
touch .env
echo VITE_LAUNCH_DARKLY_CLIENT_SIDE_ID=${{ secrets.LAUNCH_DARKLY_CLIENT_SIDE_ID }} >> .env
echo VITE_API_HOSTNAME=http://127.0.0.1:1336 >> .env
echo VITE_WEBSOCKET_HOSTNAME=http://127.0.0.1:1340 >> .env
echo VITE_LEGACY_DASHBOARD_URL=http://127.0.0.1:4200 >> .env
echo VITE_CLERK_PUBLISHABLE_KEY=${{ secrets.CLERK_PUBLISHABLE_KEY }} >> .env

- name: Create .env file for the Playwright
working-directory: apps/dashboard
run: |
touch .env.playwright
echo NOVU_ENTERPRISE=true >> .env.playwright
echo NEW_RELIC_ENABLED=false >> .env.playwright
echo NEW_RELIC_APP_NAME=Novu >> .env.playwright
echo MONGO_URL=mongodb://127.0.0.1:27017/novu-test >> .env.playwright
echo API_URL=http://127.0.0.1:1336 >> .env.playwright
echo CLERK_ENABLED=true >> .env.playwright
echo CLERK_USER_USERNAME=${{ secrets.CLERK_USER_USERNAME }} >> .env.playwright
echo CLERK_USER_PASSWORD=${{ secrets.CLERK_USER_PASSWORD }} >> .env.playwright
echo CLERK_PUBLISHABLE_KEY=${{ secrets.CLERK_PUBLISHABLE_KEY }} >> .env.playwright
echo CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY }} >> .env.playwright
echo CLERK_LONG_LIVED_TOKEN=${{ secrets.CLERK_LONG_LIVED_TOKEN }} >> .env.playwright
echo CLERK_PRIVATE_KEY=\"$(echo ${{ secrets.DASHBOARD_E2E_CLERK_PRIVATE_KEY }} | base64 -d)\" >> .env.playwright
echo CLERK_PEM_PUBLIC_KEY=\"$(echo ${{ secrets.DASHBOARD_E2E_CLERK_PEM_PUBLIC_KEY }} | base64 -d)\" >> .env.playwright
echo CLERK_EXTERNAL_ORG_ID=org_2p9RYIgFs6Z2sSdQDpnuA1mJ4dx >> .env.playwright
echo CLERK_EXTERNAL_USER_ID=user_2p7nkg2pyfTjLopPsLMh5ukMRkF >> .env.playwright

- uses: mansagroup/nrwl-nx-action@v3
with:
targets: build
Expand All @@ -77,30 +107,41 @@ jobs:
- uses: ./.github/actions/start-localstack
- uses: ./.github/actions/setup-redis-cluster

- uses: ./.github/actions/run-backend
with:
cypress_github_oauth_client_id: ${{ secrets.CYPRESS_GITHUB_OAUTH_CLIENT_ID }}
cypress_github_oauth_client_secret: ${{ secrets.CYPRESS_GITHUB_OAUTH_CLIENT_SECRET }}
launch_darkly_sdk_key: ${{ secrets.LAUNCH_DARKLY_SDK_KEY }}
ci_ee_test: ${{ steps.determine_run_type.outputs.enterprise_run }}
- name: Start API in TEST
env:
LAUNCH_DARKLY_SDK_KEY: ${{ secrets.LAUNCH_DARKLY_SDK_KEY }}
CI_EE_TEST: true
CLERK_ENABLED: true
CLERK_ISSUER_URL: ${{ vars.CLERK_ISSUER_URL }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
CLERK_PRIVATE_KEY_BASE64: ${{ secrets.CLERK_PRIVATE_KEY_BASE64 }}
CLERK_PEM_PUBLIC_KEY_BASE64: ${{ secrets.CLERK_PEM_PUBLIC_KEY_BASE64 }}
CLERK_WEBHOOK_SECRET: ${{ secrets.CLERK_WEBHOOK_SECRET }}
CLERK_LONG_LIVED_TOKEN: ${{ secrets.CLERK_LONG_LIVED_TOKEN }}
run: |
export CLERK_PEM_PUBLIC_KEY=$(echo $CLERK_PEM_PUBLIC_KEY_BASE64 | base64 -d)
export CLERK_PRIVATE_KEY=$(echo $CLERK_PRIVATE_KEY_BASE64 | base64 -d)
cd apps/api && pnpm start:test &

- name: Start Worker
shell: bash
env:
NODE_ENV: 'test'
PORT: '1342'
LAUNCH_DARKLY_SDK_KEY: ${{ secrets.LAUNCH_DARKLY_SDK_KEY }}
CI_EE_TEST: true
run: cd apps/worker && pnpm start:prod &

- name: Wait on API and Worker
shell: bash
run: wait-on --timeout=180000 http://127.0.0.1:1336/v1/health-check http://127.0.0.1:1342/v1/health-check

- name: Start WS
run: |
cd apps/ws && pnpm start:test &

- name: Start Novu Dashboard
working-directory: apps/dashboard
env:
REACT_APP_API_URL: http://127.0.0.1:1336
REACT_APP_WS_URL: http://127.0.0.1:1340
REACT_APP_WEBHOOK_URL: http://127.0.0.1:1341
# Disable LaunchDarkly client-side SDK in the test environment to reduce E2E flakiness
REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID: ''
NOVU_ENTERPRISE: ${{ steps.determine_run_type.outputs.enterprise_run }}
run: pnpm start:static:build

- name: Wait on Services
run: wait-on --timeout=180000 http://127.0.0.1:1340/v1/health-check http://127.0.0.1:8080/
run: wait-on --timeout=180000 http://127.0.0.1:1340/v1/health-check

- name: Install Playwright
working-directory: apps/dashboard
Expand Down
4 changes: 3 additions & 1 deletion apps/dashboard/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ tsconfig.node.tsbuildinfo
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/
.env.test
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env.test is the file for the Dashboard env variables

.env.playwright
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env.playwright is the file for the playwright that has all env variables to setup the clerk and api session

5 changes: 5 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"start": "vite",
"start:test": "vite --mode test",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run dashboard and use .env.test file

"start:static:build": "pm2 start proxy-server.js",
"stop:static:build": "pm2 stop proxy-server.js",
"dev": "pnpm start",
Expand Down Expand Up @@ -92,9 +93,12 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@clerk/testing": "^1.3.27",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used to initialize the Clerk session

"@clerk/types": "^4.30.0",
"@eslint/js": "^9.9.0",
"@hookform/devtools": "^4.3.0",
"@novu/dal": "workspace:*",
"@novu/testing": "workspace:*",
Comment on lines +100 to +101
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used to initialize the api session

"@playwright/test": "^1.44.0",
"@sentry/vite-plugin": "^2.22.6",
"@tiptap/core": "^2.10.3",
Expand All @@ -106,6 +110,7 @@
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"dotenv": "^16.4.5",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
Expand Down
36 changes: 31 additions & 5 deletions apps/dashboard/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import { defineConfig, devices } from '@playwright/test';
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const fileName = fileURLToPath(import.meta.url);
const dirName = dirname(fileName);
dotenv.config({ path: path.resolve(dirName, '.env.playwright') });
Comment on lines +12 to +14
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the Dashboard project is configured with "type": "module" this is how we can import the playwright env variables.


const baseURL = `http://localhost:4201`;

/**
* See https://playwright.dev/docs/test-configuration.
Expand All @@ -24,9 +31,15 @@ export default defineConfig({
/* 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. */
webServer: {
command: 'pnpm start:test',
url: baseURL,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:8080',
baseURL: baseURL,

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
Expand All @@ -38,9 +51,22 @@ export default defineConfig({
},
/* Configure projects for major browsers */
projects: [
{
name: 'setup',
testMatch: /global\.setup\.ts/,
},
Comment on lines +54 to +57
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global setup test file will initialize the Clerk session for all tests. Using this approach, we can say that for some tests, we don't want to have a session, and then we will be able to verify, for example, the sign-in/up flows.

{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.clerk/user.json',
viewport: { width: 1512, height: 982 },
video: {
mode: 'on-first-retry',
size: { width: 1512, height: 982 },
},
},
dependencies: ['setup'],
},
],
});
6 changes: 4 additions & 2 deletions apps/dashboard/src/components/primitives/tag-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Badge key={index} variant="outline" kind="tag" className="gap-1">
<span style={{ wordBreak: 'break-all' }}>{tag}</span>
<button type="button" onClick={() => removeTag(tag)}>
<span style={{ wordBreak: 'break-all' }} data-testid="tags-badge-value">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some components, I had to add data-tested as there was no accessible way to reference the elements to perform some actions.

{tag}
</span>
<button type="button" onClick={() => removeTag(tag)} data-testid={`tags-badge-remove-${tag}`}>
<RiCloseFill className="-mr-0.5 size-3" />
<span className="sr-only">Remove tag</span>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const MenuItem = ({
'text-foreground-300 cursor-not-allowed': disabled,
}
)}
data-testid={`add-step-menu-item-${stepType}`}
>
<Icon
className={`bg-neutral-alpha-50 h-6 w-6 rounded-md p-1 opacity-40`}
Expand Down Expand Up @@ -90,7 +91,7 @@ export const AddStepMenu = ({
}}
>
<PopoverTrigger asChild>
<span>
<span data-testid="add-step-menu-button">
<Node
variant="sm"
className={cn('opacity-0 transition duration-300 ease-out hover:opacity-100', {
Expand Down
16 changes: 14 additions & 2 deletions apps/dashboard/src/components/workflow-editor/in-app-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ export const InAppPreviewSubject = (props: InAppPreviewSubjectProps) => {
return <Skeleton className="h-5 w-1/2" />;
}

return <Markdown className={cn('text-foreground-600 truncate text-xs font-medium', className)} {...rest} />;
return (
<Markdown
className={cn('text-foreground-600 truncate text-xs font-medium', className)}
{...rest}
data-testid="in-app-preview-subject"
/>
);
};

type InAppPreviewBodyProps = MarkdownProps & { isPending?: boolean };
Expand All @@ -115,7 +121,13 @@ export const InAppPreviewBody = (props: InAppPreviewBodyProps) => {
);
}

return <Markdown className={cn('text-foreground-400 text-xs font-normal', className)} {...rest} />;
return (
<Markdown
className={cn('text-foreground-400 text-xs font-normal', className)}
{...rest}
data-testid="in-app-preview-body"
/>
);
};

type InAppPreviewActionsProps = HTMLAttributes<HTMLDivElement>;
Expand Down
5 changes: 4 additions & 1 deletion apps/dashboard/src/components/workflow-editor/nodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ export const InAppNode = (props: NodeProps<NodeType>) => {
const Icon = STEP_TYPE_TO_ICON[StepTypeEnum.IN_APP];

return (
<Link to={buildRoute(ROUTES.EDIT_STEP, { stepSlug: data.stepSlug ?? '' })}>
<Link
to={buildRoute(ROUTES.EDIT_STEP, { stepSlug: data.stepSlug ?? '' })}
data-testid={`${StepTypeEnum.IN_APP}-node`}
>
<StepNode data={data}>
<NodeHeader type={StepTypeEnum.IN_APP}>
<NodeIcon variant={STEP_TYPE_TO_COLOR[StepTypeEnum.IN_APP]}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const InAppAction = () => {
<DropdownMenuTrigger className="absolute size-full" />
</div>
<DropdownMenuTrigger asChild>
<Button size={'icon'} variant={'ghost'}>
<Button size={'icon'} variant={'ghost'} data-testid="in-app-action-dropdown-trigger">
<RiExpandUpDownLine className="size-4" />
</Button>
</DropdownMenuTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Notification5Fill } from '@/components/icons';
import { Separator } from '@/components/primitives/separator';
import { getComponentByType } from '@/components/workflow-editor/steps/component-utils';
import { InAppTabsSection } from '@/components/workflow-editor/steps/in-app/in-app-tabs-section';
import { FormMessage } from '../../../primitives/form/form';
import { FormMessagePure } from '../../../primitives/form/form';
import { useFormContext } from 'react-hook-form';

const avatarKey = 'avatar';
const subjectKey = 'subject';
Expand All @@ -14,6 +15,9 @@ const primaryActionKey = 'primaryAction';
const secondaryActionKey = 'secondaryAction';

export const InAppEditor = ({ uiSchema }: { uiSchema?: UiSchema }) => {
const { getFieldState } = useFormContext();
const bodyError = getFieldState(bodyKey).error;

if (!uiSchema || uiSchema?.group !== UiSchemaGroupEnum.IN_APP) {
return null;
}
Expand Down Expand Up @@ -44,7 +48,9 @@ export const InAppEditor = ({ uiSchema }: { uiSchema?: UiSchema }) => {
{body && (
<>
{getComponentByType({ component: body.component })}
<FormMessage>{`Type {{ for variables, or wrap text in ** for bold.`}</FormMessage>
<FormMessagePure
error={bodyError ? String(bodyError.message) : undefined}
>{`Type {{ for variables, or wrap text in ** for bold.`}</FormMessagePure>
</>
)}
{(primaryAction || secondaryAction) &&
Expand Down
9 changes: 7 additions & 2 deletions apps/dashboard/src/components/workflow-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => {
*/}
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<Button variant="ghost" className="h-8 w-8 p-0" data-testid="workflow-actions-menu">
<RiMore2Fill className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
Expand All @@ -256,7 +256,11 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => {
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup className="*:cursor-pointer">
<DropdownMenuItem onClick={handlePauseWorkflow} disabled={workflow.status === WorkflowStatusEnum.ERROR}>
<DropdownMenuItem
onClick={handlePauseWorkflow}
disabled={workflow.status === WorkflowStatusEnum.ERROR}
data-testid={workflow.status === WorkflowStatusEnum.ACTIVE ? 'pause-workflow' : 'enable-workflow'}
>
{workflow.status === WorkflowStatusEnum.ACTIVE ? (
<>
<RiPauseCircleLine />
Expand All @@ -275,6 +279,7 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => {
onClick={() => {
setIsDeleteModalOpen(true);
}}
data-testid="delete-workflow"
>
<RiDeleteBin2Line />
Delete workflow
Expand Down
6 changes: 6 additions & 0 deletions apps/dashboard/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { MODE } from '@/config';

// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Clerk {
export const session: {
Expand All @@ -6,5 +8,9 @@ declare namespace Clerk {
}

export async function getToken(): Promise<string> {
if (MODE === 'test') {
return localStorage.getItem('nv_auth_token') ?? '';
}
Comment on lines +11 to +13
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For tests, we store the API session-generated JWT token in the local storage because we don't use the one that is provided by the Clerk as the token signature won't match on the API, and we will not be able to authorize the requests.

In short the @novu/testing library needs some refactoring which is out of scope and might take a lot of time.


return (await Clerk.session?.getToken()) || '';
}
Loading
Loading