Skip to content

Commit 844dce9

Browse files
committed
[web-console] Add vitest unit and playwright e2e tests for the new changes
Signed-off-by: Karakatiza666 <bulakh.96@gmail.com>
1 parent 9bca012 commit 844dce9

21 files changed

+861
-91
lines changed

.github/workflows/ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ jobs:
153153
uses: ./.github/workflows/generate-sbom.yml
154154
secrets: inherit
155155

156+
invoke-tests-web-console-e2e:
157+
name: Web Console End-to-End Tests
158+
needs: [check-prior-build, invoke-build-docker]
159+
if: |
160+
always() &&
161+
(needs.invoke-build-docker.result == 'success' || needs.invoke-build-docker.result == 'skipped')
162+
uses: ./.github/workflows/test-web-console-e2e.yml
163+
secrets: inherit
164+
156165
invoke-tests-integration-platform:
157166
name: Integration Tests
158167
needs: [check-prior-build, invoke-build-docker]
@@ -298,6 +307,21 @@ jobs:
298307
-H "Accept: application/vnd.github+json" \
299308
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"
300309
310+
cancel-if-tests-web-console-integration-failed:
311+
name: Cancel if Web Console Integration Tests Failed
312+
needs: [invoke-tests-web-console-e2e]
313+
if: failure()
314+
runs-on: ubuntu-latest-amd64
315+
permissions:
316+
actions: write
317+
steps:
318+
- name: Cancel workflow
319+
run: |
320+
curl -fsSL -X POST \
321+
-H "Authorization: Bearer ${{ github.token }}" \
322+
-H "Accept: application/vnd.github+json" \
323+
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"
324+
301325
cancel-if-tests-integration-platform-failed:
302326
name: Cancel if Platform Integration Tests Failed
303327
needs: [invoke-tests-integration-platform]
@@ -370,6 +394,7 @@ jobs:
370394
- invoke-build-java
371395
- invoke-build-docs
372396
- invoke-tests-web-console-unit
397+
- invoke-tests-web-console-e2e
373398
- invoke-tests-unit
374399
- invoke-tests-adapter
375400
- invoke-build-docker
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Web Console Integration Tests
2+
3+
on:
4+
workflow_call:
5+
workflow_dispatch:
6+
7+
jobs:
8+
web-console-integration-tests:
9+
name: Web Console Integration Tests
10+
runs-on: [k8s-runners-amd64]
11+
12+
container:
13+
image: mcr.microsoft.com/playwright:v1.58.2-noble
14+
15+
services:
16+
pipeline-manager:
17+
image: ${{ vars.FELDERA_IMAGE_NAME }}:sha-${{ github.sha }}
18+
env:
19+
AUTH_PROVIDER: none
20+
RUST_LOG: info
21+
RUST_BACKTRACE: 1
22+
options: >-
23+
--health-cmd "curl --fail --silent --max-time 2 http://localhost:8080/healthz || exit 1"
24+
--health-interval 10s
25+
--health-timeout 5s
26+
--health-retries 5
27+
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v4
31+
32+
- name: Setup Bun
33+
run: |
34+
npm install -g --prefix /tmp/bun bun@1.3.10
35+
echo "/tmp/bun/bin" >> $GITHUB_PATH
36+
37+
- name: Install dependencies
38+
run: bun install --frozen-lockfile
39+
40+
- name: Sync SvelteKit
41+
run: bunx svelte-kit sync
42+
working-directory: js-packages/web-console
43+
44+
- name: Run Playwright e2e tests
45+
if: ${{ vars.CI_DRY_RUN != 'true' }}
46+
run: bun run test-e2e
47+
working-directory: js-packages/web-console
48+
env:
49+
PLAYWRIGHT_API_ORIGIN: http://pipeline-manager:8080
50+
PLAYWRIGHT_APP_ORIGIN: http://pipeline-manager:8080
51+
52+
- name: Show Kubernetes node
53+
if: always()
54+
run: |
55+
echo "K8S node: ${K8S_NODE_NAME}"

bun.lock

Lines changed: 71 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js-packages/web-console/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"@sveltejs/adapter-auto": "7.0.1",
2424
"@sveltejs/adapter-static": "3.0.10",
2525
"@sveltejs/kit": "2.52.2",
26-
"@sveltejs/vite-plugin-svelte": "6.2.4",
26+
"@sveltejs/vite-plugin-svelte": "7.0.0",
2727
"@tailwindcss/forms": "0.5.11",
2828
"@tailwindcss/typography": "0.5.19",
2929
"@tailwindcss/vite": "4.2.0",
@@ -124,7 +124,7 @@
124124
"generate-openapi": "openapi-ts && bun run format",
125125
"build-openapi": "cd ../.. && cargo run -p pipeline-manager -- --dump-openapi",
126126
"build-icons": "bun run scripts/build-webfont.ts -- -s src/assets/icons/feldera-material-icons -d src/assets/fonts/ -n feldera-material-icons -p fd --fix && bun run scripts/build-webfont.ts -- -s src/assets/icons/generic -d src/assets/fonts/ -n generic-icons -p gc && bun run format",
127-
"test-e2e": "PLAYWRIGHT_API_ORIGIN=http://localhost:8080/ PLAYWRIGHT_APP_ORIGIN=http://localhost:8080/ DISPLAY= bun playwright test",
127+
"test-e2e": "DISPLAY= bun playwright test",
128128
"test-e2e-ui": "PLAYWRIGHT_API_ORIGIN=http://localhost:8080/ PLAYWRIGHT_APP_ORIGIN=http://localhost:8080/ DISPLAY= bun playwright test --ui-host=0.0.0.0",
129129
"test-ct": "DISPLAY= bun playwright test -c playwright-ct.config.ts",
130130
"test-ct-ui": "DISPLAY= bun playwright test -c playwright-ct.config.ts --ui-host=0.0.0.0",

js-packages/web-console/playwright.config.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import type { PlaywrightTestConfig } from '@playwright/test'
22

3+
const appOrigin = process.env.PLAYWRIGHT_APP_ORIGIN
4+
35
const config: PlaywrightTestConfig = {
4-
webServer: {
5-
command: 'npm run build && npm run preview',
6-
port: 4173
7-
},
6+
// The globalSetup only runs when PLAYWRIGHT_APP_ORIGIN is set (i.e., dedicated Feldera instance), not during the local build+preview mode.
7+
globalSetup: appOrigin ? './tests/global-setup.ts' : undefined,
8+
...(appOrigin
9+
? { use: { baseURL: appOrigin } }
10+
: {
11+
webServer: {
12+
command: 'npm run build && npm run preview',
13+
port: 4173
14+
}
15+
}),
816
testDir: 'tests',
9-
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
17+
testMatch: /(.+\.)?e2e\.[jt]s/,
1018
snapshotDir: 'playwright-snapshots/e2e',
1119
expect: {
1220
toHaveScreenshot: {

js-packages/web-console/src/lib/components/dialogs/GenericDialog.svelte

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@
3333
})
3434
</script>
3535

36-
<div class="flex flex-col gap-4 p-4 sm:p-8">
36+
<div class="flex flex-col gap-4 p-4 sm:p-8" data-testid="box-generic-dialog">
3737
<div class="flex flex-nowrap justify-between">
38-
<div class="h5">{content.title}</div>
38+
<div class="h5" data-testid="box-dialog-title">{content.title}</div>
3939
{#if !noclose}
4040
<button onclick={cancel} class="fd fd-x -m-4 btn-icon text-[24px]" aria-label="Close dialog"
4141
></button>
@@ -45,22 +45,30 @@
4545
class="-mr-4 scrollbar flex max-h-[calc(90vh-96px)] flex-col gap-4 overflow-auto pr-4 sm:-mr-8"
4646
>
4747
{#if content.description}
48-
<span class="whitespace-pre-wrap">
48+
<span class="whitespace-pre-wrap" data-testid="box-dialog-description">
4949
{content.description}
5050
</span>
5151
{/if}
5252
{#if content.scrollableContent}
5353
<div
5454
class="bg-surface-100-800 scrollbar max-h-[60vh] overflow-y-auto rounded border p-2 whitespace-pre-wrap"
55+
data-testid="box-dialog-scrollable-content"
5556
>
5657
{content.scrollableContent}
5758
</div>
5859
{/if}
5960
{@render children?.()}
6061
</div>
6162
{#if content.onSuccess}
62-
<div class="flex w-full flex-col-reverse gap-4 sm:flex-row sm:justify-end">
63-
<button onclick={() => cancel()} class="btn preset-filled-surface-50-950 px-4">
63+
<div
64+
class="flex w-full flex-col-reverse gap-4 sm:flex-row sm:justify-end"
65+
data-testid="box-dialog-actions"
66+
>
67+
<button
68+
onclick={() => cancel()}
69+
class="btn preset-filled-surface-50-950 px-4"
70+
data-testid="btn-dialog-cancel"
71+
>
6472
{content.onCancel?.name ?? 'Cancel'}
6573
</button>
6674
<div>
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { page } from 'vitest/browser'
3+
import { render } from 'vitest-browser-svelte'
4+
import type { GlobalDialogContent } from '$lib/compositions/layout/useGlobalDialog.svelte'
5+
import GenericDialog from './GenericDialog.svelte'
6+
7+
function makeContent(overrides?: Partial<GlobalDialogContent>): GlobalDialogContent {
8+
return {
9+
title: 'Test Dialog',
10+
...overrides
11+
}
12+
}
13+
14+
async function renderDialog(
15+
content: GlobalDialogContent,
16+
opts?: { danger?: boolean; disabled?: boolean; noclose?: boolean }
17+
) {
18+
return render(GenericDialog, { content, ...opts })
19+
}
20+
21+
describe('GenericDialog.svelte', () => {
22+
describe('A. Basic rendering', () => {
23+
it('renders title', async () => {
24+
await renderDialog(makeContent({ title: 'My Title' }))
25+
await expect.element(page.getByTestId('box-dialog-title')).toHaveTextContent('My Title')
26+
})
27+
28+
it('renders description when provided', async () => {
29+
await renderDialog(makeContent({ description: 'Some description text' }))
30+
await expect
31+
.element(page.getByTestId('box-dialog-description'))
32+
.toHaveTextContent('Some description text')
33+
})
34+
35+
it('does not render description when omitted', async () => {
36+
await renderDialog(makeContent())
37+
await expect.element(page.getByTestId('box-dialog-description')).not.toBeInTheDocument()
38+
})
39+
40+
it('renders scrollable content when provided', async () => {
41+
await renderDialog(makeContent({ scrollableContent: 'Scrollable text here' }))
42+
await expect
43+
.element(page.getByTestId('box-dialog-scrollable-content'))
44+
.toHaveTextContent('Scrollable text here')
45+
})
46+
47+
it('does not render scrollable content when omitted', async () => {
48+
await renderDialog(makeContent())
49+
await expect
50+
.element(page.getByTestId('box-dialog-scrollable-content'))
51+
.not.toBeInTheDocument()
52+
})
53+
})
54+
55+
describe('B. Action buttons', () => {
56+
it('renders success and cancel buttons when onSuccess is provided', async () => {
57+
await renderDialog(
58+
makeContent({
59+
onSuccess: {
60+
name: 'Confirm',
61+
callback: vi.fn(),
62+
'data-testid': 'btn-dialog-success'
63+
}
64+
})
65+
)
66+
await expect.element(page.getByTestId('btn-dialog-success')).toHaveTextContent('Confirm')
67+
await expect.element(page.getByTestId('btn-dialog-cancel')).toHaveTextContent('Cancel')
68+
})
69+
70+
it('does not render action buttons when onSuccess is omitted', async () => {
71+
await renderDialog(makeContent())
72+
await expect.element(page.getByTestId('box-dialog-actions')).not.toBeInTheDocument()
73+
})
74+
75+
it('calls onSuccess callback when confirm button is clicked', async () => {
76+
const onSuccess = vi.fn()
77+
await renderDialog(
78+
makeContent({
79+
onSuccess: { name: 'Confirm', callback: onSuccess, 'data-testid': 'btn-dialog-success' }
80+
})
81+
)
82+
await page.getByTestId('btn-dialog-success').click()
83+
expect(onSuccess).toHaveBeenCalledOnce()
84+
})
85+
86+
it('uses custom cancel button label from onCancel.name', async () => {
87+
await renderDialog(
88+
makeContent({
89+
onSuccess: { name: 'OK', callback: vi.fn() },
90+
onCancel: { name: 'Go Back' }
91+
})
92+
)
93+
await expect.element(page.getByTestId('btn-dialog-cancel')).toHaveTextContent('Go Back')
94+
})
95+
96+
it('renders data-testid on success button when provided', async () => {
97+
await renderDialog(
98+
makeContent({
99+
onSuccess: {
100+
name: 'Apply',
101+
callback: vi.fn(),
102+
'data-testid': 'button-confirm-apply'
103+
}
104+
})
105+
)
106+
await expect.element(page.getByTestId('button-confirm-apply')).toBeInTheDocument()
107+
})
108+
})
109+
110+
describe('C. Close button and noclose', () => {
111+
it('renders close X button by default', async () => {
112+
await renderDialog(makeContent())
113+
await expect.element(page.getByLabelText('Close dialog')).toBeInTheDocument()
114+
})
115+
116+
it('hides close X button when noclose is set', async () => {
117+
await renderDialog(makeContent(), { noclose: true })
118+
await expect.element(page.getByLabelText('Close dialog')).not.toBeInTheDocument()
119+
})
120+
})
121+
122+
describe('D. Disabled state', () => {
123+
it('disables success button when disabled prop is set', async () => {
124+
await renderDialog(
125+
makeContent({
126+
onSuccess: { name: 'Apply', callback: vi.fn(), 'data-testid': 'btn-dialog-success' }
127+
}),
128+
{ disabled: true }
129+
)
130+
await expect.element(page.getByTestId('btn-dialog-success')).toBeDisabled()
131+
})
132+
133+
it('success button is enabled by default', async () => {
134+
await renderDialog(
135+
makeContent({
136+
onSuccess: { name: 'Apply', callback: vi.fn(), 'data-testid': 'btn-dialog-success' }
137+
})
138+
)
139+
await expect.element(page.getByTestId('btn-dialog-success')).not.toBeDisabled()
140+
})
141+
})
142+
143+
describe('E. Cancel callback', () => {
144+
it('calls onCancel callback when cancel button is clicked', async () => {
145+
const onCancel = vi.fn()
146+
await renderDialog(
147+
makeContent({
148+
onSuccess: { name: 'OK', callback: vi.fn() },
149+
onCancel: { name: 'Back', callback: onCancel }
150+
})
151+
)
152+
await page.getByTestId('btn-dialog-cancel').click()
153+
expect(onCancel).toHaveBeenCalledOnce()
154+
})
155+
156+
it('calls onCancel callback when close X is clicked', async () => {
157+
const onCancel = vi.fn()
158+
await renderDialog(
159+
makeContent({
160+
onCancel: { callback: onCancel }
161+
})
162+
)
163+
await page.getByLabelText('Close dialog').click()
164+
expect(onCancel).toHaveBeenCalledOnce()
165+
})
166+
})
167+
})

0 commit comments

Comments
 (0)