Add Playwright e2e coverage and Node 20 pin

This commit is contained in:
chihlasm
2026-03-16 02:28:04 -04:00
parent 357f8e2d08
commit e39819f8d0
27 changed files with 1743 additions and 7 deletions

86
frontend/e2e/README.md Normal file
View File

@@ -0,0 +1,86 @@
# Playwright E2E
ResolutionFlow's Playwright suite is still intentionally lean, but it now covers the main routes and workflows most likely to break demos, onboarding, or day-one product usage.
## What it covers
- Public landing page
- Redirect behavior for protected routes
- UI login flow
- Authenticated dashboard shell
- Session history page
- Session history filtering
- Feedback page
- Account settings page
- Profile save flow
- Flow library search
- Start session directly from library search results
- Resume incomplete session
- Start and complete a troubleshooting session
- Export preview for a completed session
- Personal analytics page
- Team analytics page
- Shared sessions exports page
- Public shared session page
## How auth works
- `auth.setup.ts` logs in through the backend API using the seeded team admin user
- it writes a Playwright storage state file to `e2e/.auth/team-admin.json`
- most authenticated specs reuse that state
- `auth.spec.ts`, `public.spec.ts`, and `shared-session.spec.ts` explicitly clear storage so those tests stay truly unauthenticated
## Local prerequisites
1. PostgreSQL must be running
2. `backend/venv` should exist with backend dependencies installed
3. frontend dependencies must be installed
4. Playwright Chromium must be installed once
## First-time setup
```bash
cd frontend
npm install
npx playwright install chromium
```
## Run the suite
```bash
cd frontend
npm run test:e2e
```
Useful variants:
```bash
npm run test:e2e -- --list
npm run test:e2e:headed
npm run test:e2e:ui
npm run test:e2e:debug
```
## Backend boot behavior
The Playwright config starts the backend automatically. It will:
1. run Alembic migrations
2. seed test users
3. launch Uvicorn on `127.0.0.1:8000`
Then it builds and serves the frontend via `vite preview` on `127.0.0.1:4173`.
## Default test credentials
- Email: `teamadmin@resolutionflow.example.com`
- Password: `TestPass123!`
Override them with:
- `PLAYWRIGHT_TEST_EMAIL`
- `PLAYWRIGHT_TEST_PASSWORD`
## Common local issue
If the suite fails before tests start with a database connection error, PostgreSQL is not running or the Playwright DB env vars do not point at a reachable database.

View File

@@ -0,0 +1,44 @@
import { expect, test } from '@playwright/test'
import {
completeSession,
createAuthenticatedApiContext,
createSession,
createTroubleshootingTree,
disposeApiContext,
uniqueName,
} from './helpers/api'
test.describe('analytics smoke tests', () => {
test('personal and team analytics pages load for a team admin', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const tree = await createTroubleshootingTree(api, {
name: uniqueName('Playwright Analytics Flow'),
})
const session = await createSession(api, tree.id, {
ticket_number: `PW-ANALYTICS-${Date.now()}`,
client_name: 'Analytics Client',
})
await completeSession(api, session.id, {
outcome: 'resolved',
outcome_notes: 'Analytics smoke test session',
})
try {
await page.goto('/analytics/me')
await expect(
page.getByRole('heading', { name: 'My Analytics' }),
).toBeVisible()
await expect(page.getByText('My Sessions', { exact: true })).toBeVisible()
await page.goto('/analytics')
await expect(
page.getByRole('heading', { name: 'Team Analytics' }),
).toBeVisible()
await expect(page.getByText('Total Sessions', { exact: true })).toBeVisible()
} finally {
await disposeApiContext(api)
}
})
})

View File

@@ -0,0 +1,68 @@
import { expect, test as setup } from '@playwright/test'
import { mkdir, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
type TokenResponse = {
access_token: string
refresh_token: string
token_type: string
}
const frontendOrigin = new URL(
process.env.PLAYWRIGHT_BASE_URL || 'http://127.0.0.1:4173',
).origin
const apiOrigin = process.env.PLAYWRIGHT_API_ORIGIN || 'http://127.0.0.1:8000'
const testEmail =
process.env.PLAYWRIGHT_TEST_EMAIL || 'teamadmin@resolutionflow.example.com'
const testPassword =
process.env.PLAYWRIGHT_TEST_PASSWORD || 'TestPass123!'
const authDir = fileURLToPath(new URL('./.auth/', import.meta.url))
const authFile = path.join(authDir, 'team-admin.json')
setup('authenticate seeded team admin and persist storage state', async ({ request }) => {
const response = await request.post(`${apiOrigin}/api/v1/auth/login/json`, {
data: {
email: testEmail,
password: testPassword,
},
})
expect(response.ok()).toBeTruthy()
const token = (await response.json()) as TokenResponse
const authStorage = JSON.stringify({
state: {
token,
isAuthenticated: true,
account: null,
subscription: null,
},
version: 0,
})
await mkdir(authDir, { recursive: true })
await writeFile(
authFile,
JSON.stringify(
{
cookies: [],
origins: [
{
origin: frontendOrigin,
localStorage: [
{ name: 'access_token', value: token.access_token },
{ name: 'refresh_token', value: token.refresh_token },
{ name: 'auth-storage', value: authStorage },
],
},
],
},
null,
2,
),
'utf8',
)
})

13
frontend/e2e/auth.spec.ts Normal file
View File

@@ -0,0 +1,13 @@
import { expect, test } from '@playwright/test'
import { signIn } from './helpers/auth'
test.use({ storageState: { cookies: [], origins: [] } })
test.describe('authentication smoke tests', () => {
test('team admin can sign in through the login form', async ({ page }) => {
await signIn(page)
await expect(page).toHaveURL(/\/$/)
await expect(page.getByTestId('app-shell')).toBeVisible()
})
})

196
frontend/e2e/helpers/api.ts Normal file
View File

@@ -0,0 +1,196 @@
import { expect, request as playwrightRequest, type APIRequestContext } from '@playwright/test'
type TokenResponse = {
access_token: string
refresh_token: string
token_type: string
}
type TreeResponse = {
id: string
name: string
description?: string | null
tree_type: string
tree_structure: Record<string, unknown>
}
type SessionResponse = {
id: string
tree_id: string
started_at: string | null
completed_at: string | null
ticket_number: string | null
client_name: string | null
}
type SessionShareResponse = {
id: string
session_id: string
share_token: string
share_name: string | null
visibility: 'public' | 'account'
is_active: boolean
}
const apiOrigin = process.env.PLAYWRIGHT_API_ORIGIN || 'http://127.0.0.1:8000'
const testEmail =
process.env.PLAYWRIGHT_TEST_EMAIL || 'teamadmin@resolutionflow.example.com'
const testPassword =
process.env.PLAYWRIGHT_TEST_PASSWORD || 'TestPass123!'
export async function createAuthenticatedApiContext() {
const authRequest = await playwrightRequest.newContext()
const authResponse = await authRequest.post(`${apiOrigin}/api/v1/auth/login/json`, {
data: {
email: testEmail,
password: testPassword,
},
})
expect(authResponse.ok()).toBeTruthy()
const token = (await authResponse.json()) as TokenResponse
await authRequest.dispose()
return playwrightRequest.newContext({
baseURL: `${apiOrigin}/api/v1/`,
extraHTTPHeaders: {
Authorization: `Bearer ${token.access_token}`,
'Content-Type': 'application/json',
},
})
}
export async function disposeApiContext(api: APIRequestContext) {
await api.dispose()
}
export function uniqueName(prefix: string) {
return `${prefix} ${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
export async function createTroubleshootingTree(
api: APIRequestContext,
overrides?: Partial<{
name: string
description: string
question: string
answerLabel: string
alternateAnswerLabel: string
solutionTitle: string
solutionDescription: string
alternateSolutionTitle: string
alternateSolutionDescription: string
ticketNumber: string
}>,
) {
const treeName = overrides?.name || uniqueName('Playwright Troubleshooting Flow')
const question = overrides?.question || 'Can you reproduce the issue?'
const answerLabel = overrides?.answerLabel || 'Yes'
const alternateAnswerLabel = overrides?.alternateAnswerLabel || 'No'
const solutionTitle = overrides?.solutionTitle || 'Apply the known fix'
const solutionDescription = overrides?.solutionDescription || 'Run the standard remediation steps'
const alternateSolutionTitle = overrides?.alternateSolutionTitle || 'Collect more details and escalate'
const alternateSolutionDescription =
overrides?.alternateSolutionDescription || 'Escalate with the collected troubleshooting evidence'
const response = await api.post('trees', {
data: {
name: treeName,
description: overrides?.description || 'Playwright-created troubleshooting flow',
category: 'Playwright',
tree_type: 'troubleshooting',
tree_structure: {
id: 'root',
type: 'decision',
question,
options: [
{ id: 'opt-yes', label: answerLabel, next_node_id: 'fix-step' },
{ id: 'opt-no', label: alternateAnswerLabel, next_node_id: 'escalate-step' },
],
children: [
{
id: 'fix-step',
type: 'solution',
title: solutionTitle,
description: solutionDescription,
solution: 'Issue resolved with standard steps',
},
{
id: 'escalate-step',
type: 'solution',
title: alternateSolutionTitle,
description: alternateSolutionDescription,
solution: 'Escalate to the next support tier',
},
],
},
status: 'published',
},
})
expect(response.ok()).toBeTruthy()
return (await response.json()) as TreeResponse
}
export async function createSession(
api: APIRequestContext,
treeId: string,
overrides?: Partial<{
ticket_number: string
client_name: string
}>,
) {
const response = await api.post('sessions', {
data: {
tree_id: treeId,
ticket_number: overrides?.ticket_number || `PW-${Date.now()}`,
client_name: overrides?.client_name || 'Playwright Client',
},
})
expect(response.ok()).toBeTruthy()
return (await response.json()) as SessionResponse
}
export async function completeSession(
api: APIRequestContext,
sessionId: string,
overrides?: Partial<{
outcome: 'resolved' | 'workaround' | 'escalated' | 'unresolved'
outcome_notes: string
next_steps: string
}>,
) {
const response = await api.post(`sessions/${sessionId}/complete`, {
data: {
outcome: overrides?.outcome || 'resolved',
outcome_notes: overrides?.outcome_notes || 'Completed by Playwright',
next_steps: overrides?.next_steps || 'No follow-up required',
},
})
expect(response.ok()).toBeTruthy()
return (await response.json()) as SessionResponse
}
export async function createSessionShare(
api: APIRequestContext,
sessionId: string,
overrides?: Partial<{
visibility: 'public' | 'account'
share_name: string
expires_at: string
}>,
) {
const response = await api.post(`sessions/${sessionId}/shares`, {
data: {
visibility: overrides?.visibility || 'public',
share_name: overrides?.share_name,
expires_at: overrides?.expires_at,
},
})
expect(response.ok()).toBeTruthy()
return (await response.json()) as SessionShareResponse
}

View File

@@ -0,0 +1,17 @@
import { expect, type Page } from '@playwright/test'
const TEST_USER_EMAIL =
process.env.PLAYWRIGHT_TEST_EMAIL || 'teamadmin@resolutionflow.example.com'
const TEST_USER_PASSWORD =
process.env.PLAYWRIGHT_TEST_PASSWORD || 'TestPass123!'
export async function signIn(page: Page) {
await page.goto('/login')
await expect(page.getByTestId('login-form')).toBeVisible()
await page.getByLabel('Email address').fill(TEST_USER_EMAIL)
await page.getByLabel('Password').fill(TEST_USER_PASSWORD)
await page.getByTestId('login-submit').click()
await expect(page.getByTestId('app-shell')).toBeVisible()
}

View File

@@ -0,0 +1,45 @@
import { expect, test } from '@playwright/test'
import {
createAuthenticatedApiContext,
createSession,
createTroubleshootingTree,
disposeApiContext,
uniqueName,
} from './helpers/api'
test.describe('session history smoke tests', () => {
test('can filter sessions by ticket and client, then open details', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const ticketNumber = `PW-HISTORY-${Date.now()}`
const clientName = uniqueName('History Client')
const tree = await createTroubleshootingTree(api, {
name: uniqueName('Playwright History Flow'),
})
const session = await createSession(api, tree.id, {
ticket_number: ticketNumber,
client_name: clientName,
})
try {
await page.goto('/sessions')
await expect(
page.getByRole('heading', { name: 'Session History' }),
).toBeVisible()
await page.getByPlaceholder('Search by ticket number...').fill(ticketNumber)
await page.getByPlaceholder('Search by client name...').fill(clientName)
const sessionCard = page.locator('.bg-card').filter({ hasText: ticketNumber }).filter({ hasText: clientName }).first()
await expect(sessionCard).toBeVisible()
await expect(sessionCard.getByText(tree.name)).toBeVisible()
await sessionCard.getByRole('button', { name: 'View Details' }).click()
await expect(page).toHaveURL(new RegExp(`/sessions/${session.id}$`))
await expect(page.getByRole('heading', { name: ticketNumber })).toBeVisible()
} finally {
await disposeApiContext(api)
}
})
})

View File

@@ -0,0 +1,46 @@
import { expect, test } from '@playwright/test'
import {
createAuthenticatedApiContext,
createTroubleshootingTree,
disposeApiContext,
uniqueName,
} from './helpers/api'
test.describe('flow library start-session smoke tests', () => {
test('can start a troubleshooting session directly from a search result', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const tree = await createTroubleshootingTree(api, {
name: uniqueName('Playwright Direct Start Flow'),
question: 'Did the library launch open the flow?',
})
try {
await page.goto('/trees')
await expect(
page.getByRole('heading', { name: 'Flow Library' }),
).toBeVisible()
await page.getByPlaceholder('Search flows...').fill(tree.name)
await page.getByRole('button', { name: 'Search', exact: true }).click()
const treeCard = page.locator('.bg-card').filter({ hasText: tree.name }).first()
await expect(treeCard).toBeVisible()
await treeCard.getByRole('button', { name: /^Start(?: Session)?$/ }).click()
await expect(page).toHaveURL(new RegExp(`/trees/${tree.id}/navigate$`))
await expect(page.getByRole('heading', { name: tree.name })).toBeVisible()
await expect(page.getByRole('button', { name: 'Start Troubleshooting' })).toBeVisible()
await page.getByPlaceholder('e.g., INC0012345').fill('PW-DIRECT-START')
await page.getByPlaceholder('e.g., Acme Corp').fill('Direct Start Client')
await page.getByRole('button', { name: 'Start Troubleshooting' }).click()
await expect(
page.getByRole('heading', { name: 'Did the library launch open the flow?' }),
).toBeVisible()
} finally {
await disposeApiContext(api)
}
})
})

View File

@@ -0,0 +1,28 @@
import { expect, test } from '@playwright/test'
import {
createAuthenticatedApiContext,
createTroubleshootingTree,
disposeApiContext,
} from './helpers/api'
test.describe('flow library smoke tests', () => {
test('can search for an API-created troubleshooting flow', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const tree = await createTroubleshootingTree(api)
try {
await page.goto('/trees')
await expect(
page.getByRole('heading', { name: 'Flow Library' }),
).toBeVisible()
await page.getByPlaceholder('Search flows...').fill(tree.name)
await page.getByRole('button', { name: 'Search', exact: true }).click()
await expect(page.getByText(tree.name)).toBeVisible()
} finally {
await disposeApiContext(api)
}
})
})

View File

@@ -0,0 +1,36 @@
import { expect, test } from '@playwright/test'
test.describe('authenticated navigation smoke tests', () => {
test('dashboard loads for an authenticated user', async ({ page }) => {
await page.goto('/')
await expect(page.getByTestId('app-shell')).toBeVisible()
await expect(
page.getByRole('heading', { name: /Good (morning|afternoon|evening)/ }),
).toBeVisible()
})
test('session history page loads', async ({ page }) => {
await page.goto('/sessions')
await expect(
page.getByRole('heading', { name: 'Session History' }),
).toBeVisible()
})
test('feedback page loads', async ({ page }) => {
await page.goto('/feedback')
await expect(
page.getByRole('heading', { name: 'Send Feedback' }),
).toBeVisible()
})
test('account settings page loads', async ({ page }) => {
await page.goto('/account')
await expect(
page.getByRole('heading', { name: 'Account Settings' }),
).toBeVisible()
})
})

View File

@@ -0,0 +1,20 @@
import { expect, test } from '@playwright/test'
import { uniqueName } from './helpers/api'
test.describe('profile settings smoke tests', () => {
test('can update the job title', async ({ page }) => {
const jobTitle = uniqueName('Playwright Engineer')
await page.goto('/account/profile')
await expect(
page.getByRole('heading', { name: 'Profile Settings' }),
).toBeVisible()
await page.getByLabel('Job Title').fill(jobTitle)
await page.getByRole('button', { name: 'Save Changes' }).click()
await expect(page.getByText('Profile updated')).toBeVisible()
await expect(page.getByLabel('Job Title')).toHaveValue(jobTitle)
})
})

View File

@@ -0,0 +1,25 @@
import { expect, test } from '@playwright/test'
test.use({ storageState: { cookies: [], origins: [] } })
test.describe('public route smoke tests', () => {
test('landing page loads', async ({ page }) => {
await page.goto('/landing')
await expect(
page.getByRole('link', { name: 'Get Started Free' }),
).toBeVisible()
await expect(
page.getByText('Stop writing ticket notes.'),
).toBeVisible()
})
test('protected routes redirect unauthenticated users to landing', async ({ page }) => {
await page.goto('/sessions')
await expect(page).toHaveURL(/\/landing$/)
await expect(
page.getByRole('link', { name: 'Sign In' }),
).toBeVisible()
})
})

View File

@@ -0,0 +1,38 @@
import { expect, test } from '@playwright/test'
import {
createAuthenticatedApiContext,
createSession,
createTroubleshootingTree,
disposeApiContext,
} from './helpers/api'
test.describe('session resume smoke tests', () => {
test('can resume an incomplete session from the library page', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const tree = await createTroubleshootingTree(api, {
question: 'Is the affected service still down?',
})
await createSession(api, tree.id, {
ticket_number: 'PW-RESUME',
client_name: 'Resume Client',
})
try {
await page.goto('/trees')
const resumeCard = page.locator('.bg-card').filter({ hasText: tree.name }).filter({ hasText: 'Resume' }).first()
await expect(resumeCard).toBeVisible()
await resumeCard.getByRole('button', { name: 'Resume' }).first().click()
await expect(page).toHaveURL(new RegExp(`/trees/${tree.id}/navigate`))
await expect(
page.getByRole('heading', { name: tree.name }),
).toBeVisible()
await expect(
page.getByRole('heading', { name: 'Is the affected service still down?' }),
).toBeVisible()
} finally {
await disposeApiContext(api)
}
})
})

View File

@@ -0,0 +1,89 @@
import { expect, test } from '@playwright/test'
import {
completeSession,
createAuthenticatedApiContext,
createSession,
createTroubleshootingTree,
disposeApiContext,
} from './helpers/api'
test.describe('session workflow smoke tests', () => {
test('can start and complete a troubleshooting session through the UI', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const tree = await createTroubleshootingTree(api, {
question: 'Did the restart resolve the issue?',
answerLabel: 'Yes, it is fixed',
solutionTitle: 'Document the fix and close the ticket',
})
try {
await page.goto(`/trees/${tree.id}/navigate`)
await expect(
page.getByRole('heading', { name: tree.name }),
).toBeVisible()
await page.getByPlaceholder('e.g., INC0012345').fill('PW-START-COMPLETE')
await page.getByPlaceholder('e.g., Acme Corp').fill('Workflow Client')
await page.getByRole('button', { name: 'Start Troubleshooting' }).click()
await expect(
page.getByRole('heading', { name: 'Did the restart resolve the issue?' }),
).toBeVisible()
await page.getByRole('button', { name: 'Yes, it is fixed' }).click()
await expect(
page.getByRole('heading', { name: 'Document the fix and close the ticket' }),
).toBeVisible()
await page.getByRole('button', { name: 'Complete Session' }).click()
const outcomeDialog = page.getByRole('dialog', { name: 'Session Outcome' })
await expect(outcomeDialog).toBeVisible()
await outcomeDialog.getByPlaceholder('Add context for this outcome...').fill('Playwright verified the UI completion flow.')
await outcomeDialog.getByRole('button', { name: 'Complete Session' }).click()
const csatDialog = page.getByRole('dialog', { name: 'How was this flow?' })
await expect(csatDialog).toBeVisible()
await csatDialog.getByRole('button', { name: 'Skip' }).click()
await expect(page).toHaveURL(/\/sessions\/[0-9a-f-]+$/)
await expect(page.getByText('Resolved')).toBeVisible()
await expect(page.getByText('PW-START-COMPLETE')).toBeVisible()
} finally {
await disposeApiContext(api)
}
})
test('can preview an export for a completed session', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const tree = await createTroubleshootingTree(api, {
name: 'Playwright Export Flow',
})
const session = await createSession(api, tree.id, {
ticket_number: 'PW-EXPORT',
client_name: 'Export Client',
})
await completeSession(api, session.id, {
outcome_notes: 'Completed for export preview verification',
})
try {
await page.goto(`/sessions/${session.id}`)
await expect(
page.getByRole('heading', { name: 'PW-EXPORT' }),
).toBeVisible()
await page.getByRole('button', { name: 'Preview' }).click()
const previewDialog = page.getByRole('dialog', { name: 'Export Preview' })
await expect(previewDialog).toBeVisible()
await expect(previewDialog.getByLabel('Export content')).not.toHaveValue('')
await expect(previewDialog.getByLabel('Export content')).toHaveValue(/PW-EXPORT/)
} finally {
await disposeApiContext(api)
}
})
})

View File

@@ -0,0 +1,46 @@
import { expect, test } from '@playwright/test'
import {
completeSession,
createAuthenticatedApiContext,
createSession,
createSessionShare,
createTroubleshootingTree,
disposeApiContext,
uniqueName,
} from './helpers/api'
test.use({ storageState: { cookies: [], origins: [] } })
test.describe('shared session smoke tests', () => {
test('a public share opens for an unauthenticated viewer', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const tree = await createTroubleshootingTree(api, {
name: uniqueName('Playwright Shared Session Flow'),
})
const session = await createSession(api, tree.id, {
ticket_number: `PW-SHARE-${Date.now()}`,
client_name: 'Shared Session Client',
})
await completeSession(api, session.id, {
outcome: 'resolved',
outcome_notes: 'Shared session smoke test',
})
const share = await createSessionShare(api, session.id, {
visibility: 'public',
share_name: 'Playwright Customer Share',
})
try {
await page.goto(`/share/${share.share_token}`)
await expect(
page.getByRole('heading', { name: 'Playwright Customer Share' }),
).toBeVisible()
await expect(page.getByText('Can you reproduce the issue?', { exact: true })).toBeVisible()
await expect(page.getByText(`Ticket: #${session.ticket_number}`)).toBeVisible()
await expect(page.getByText(`Client: ${session.client_name}`)).toBeVisible()
} finally {
await disposeApiContext(api)
}
})
})

View File

@@ -0,0 +1,46 @@
import { expect, test } from '@playwright/test'
import {
createAuthenticatedApiContext,
createSession,
createSessionShare,
createTroubleshootingTree,
disposeApiContext,
uniqueName,
} from './helpers/api'
test.describe('shared session management smoke tests', () => {
test('created shares appear in exports and can be revoked', async ({ page }) => {
const api = await createAuthenticatedApiContext()
const tree = await createTroubleshootingTree(api, {
name: uniqueName('Playwright Exports Flow'),
})
const session = await createSession(api, tree.id, {
ticket_number: `PW-EXPORTS-${Date.now()}`,
client_name: 'Exports Client',
})
const share = await createSessionShare(api, session.id, {
visibility: 'public',
share_name: uniqueName('Playwright Exports Share'),
})
try {
await page.goto('/shares')
await expect(
page.getByRole('heading', { name: 'My Shared Sessions' }),
).toBeVisible()
await expect(page.getByText(share.share_name || '')).toBeVisible()
const shareCard = page.locator('.bg-card').filter({ hasText: share.share_name || '' }).first()
await shareCard.getByRole('button', { name: 'Revoke' }).click()
const confirmDialog = page.getByRole('dialog', { name: 'Revoke Share Link' })
await expect(confirmDialog).toBeVisible()
await confirmDialog.getByRole('button', { name: 'Revoke' }).click()
await expect(page.getByText(share.share_name || '')).not.toBeVisible()
} finally {
await disposeApiContext(api)
}
})
})