smoke.spec.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /**
  2. * Playwright end-to-end smoke test
  3. *
  4. * Preconditions (operator must supply):
  5. * - The backend is running on port 8080 with the built UI served via --webui-dir
  6. * - SVAPI_ADMIN_KEY env var contains the admin API key (e.g. "e2eadmin")
  7. * - SVAPI_BASE env var (optional) overrides the base URL (default http://localhost:8080)
  8. *
  9. * Run sequence (example):
  10. * cmake --build build -j"$(nproc)"
  11. * SMARTBOTIC_VECTORAPI_KEY=e2eadmin ./build/src/smartbotic-vectorapi \
  12. * --config config/config.json --share-dir api --webui-dir build/webui/dist &
  13. * cd webui && SVAPI_ADMIN_KEY=e2eadmin SVAPI_BASE=http://localhost:8080 npx playwright test
  14. *
  15. * The test creates a collection "e2e_docs" and a document inside it, then
  16. * deletes the collection (cleanup) before logging out.
  17. */
  18. import { test, expect } from '@playwright/test'
  19. const ADMIN_KEY = process.env['SVAPI_ADMIN_KEY'] ?? 'e2eadmin'
  20. test('login → collections → documents → logout smoke', async ({ page, context }) => {
  21. // Monaco mangles typed/inserted JSON via auto-closing brackets, so we paste
  22. // the document body via the clipboard instead — which needs this permission.
  23. await context.grantPermissions(['clipboard-read', 'clipboard-write'])
  24. // Best-effort cleanup so the test is idempotent: a prior failed run may have
  25. // left the e2e_docs collection behind, which would make the create step fail.
  26. await page.request.delete('/api/v1/projects/default/collections/e2e_docs', {
  27. headers: { Authorization: `Bearer ${ADMIN_KEY}` },
  28. })
  29. // ── 1. Navigate to / → should redirect to /login ─────────────────────────
  30. await page.goto('/')
  31. await expect(page).toHaveURL(/\/login/)
  32. // ── 2. Fill API key and submit ────────────────────────────────────────────
  33. await page.locator('[data-testid="api-key-input"]').fill(ADMIN_KEY)
  34. await page.locator('[data-testid="login-submit-btn"]').click()
  35. // After login, should land on dashboard (root path)
  36. await expect(page).toHaveURL(/\/$/)
  37. // Project selector should be visible and show "default"
  38. const projectSelector = page.getByRole('combobox', { name: 'Select project' })
  39. await expect(projectSelector).toBeVisible()
  40. await expect(projectSelector).toHaveValue('default')
  41. // ── 3. Navigate to Collections ────────────────────────────────────────────
  42. // Scope to the sidebar — the Documents page also renders a "Collections"
  43. // breadcrumb link, so an unscoped role query would match two elements.
  44. const sidebar = page.locator('aside')
  45. await sidebar.getByRole('link', { name: 'Collections' }).click()
  46. await expect(page).toHaveURL(/\/collections/)
  47. // Open "New collection" modal
  48. await page.locator('[data-testid="new-collection-btn"]').click()
  49. // Fill collection name
  50. const nameInput = page.locator('[data-testid="collection-name-input"]')
  51. await expect(nameInput).toBeVisible()
  52. await nameInput.fill('e2e_docs')
  53. // Kind is already "json" by default — just submit
  54. await page.locator('[data-testid="collection-create-btn"]').click()
  55. // Wait for modal to close and collection to appear
  56. await expect(page.locator('[data-testid="collection-row-e2e_docs"]')).toBeVisible({ timeout: 10_000 })
  57. // ── 4. Click the collection row → navigates to its documents ──────────────
  58. await page.locator('[data-testid="collection-row-e2e_docs"]').click()
  59. await expect(page).toHaveURL(/\/documents\?collection=e2e_docs/)
  60. // Collection picker should be pre-selected from the `?collection=` param
  61. const collPicker = page.locator('[data-testid="collection-picker"]')
  62. await expect(collPicker).toBeVisible()
  63. await expect(collPicker).toHaveValue('e2e_docs')
  64. // Click "New document"
  65. const newDocBtn = page.locator('[data-testid="new-doc-btn"]')
  66. await expect(newDocBtn).toBeEnabled({ timeout: 5_000 })
  67. await newDocBtn.click()
  68. // Monaco editor: wait for it to load and type the document
  69. // Monaco renders a contenteditable div; we use keyboard to replace the default "{}"
  70. const monacoEditor = page.locator('.monaco-editor').first()
  71. await expect(monacoEditor).toBeVisible({ timeout: 15_000 })
  72. // Click into the editor, select all and replace with our JSON. Pasting via
  73. // the clipboard avoids Monaco's auto-closing brackets, which corrupt the JSON
  74. // when typing or inserting character data.
  75. await monacoEditor.click()
  76. await page.keyboard.press('Control+a')
  77. await page.evaluate((t) => navigator.clipboard.writeText(t), '{"name":"e2e"}')
  78. await page.keyboard.press('Control+v')
  79. // Save button
  80. const saveBtn = page.locator('[data-testid="doc-save-btn"]')
  81. await expect(saveBtn).toBeEnabled({ timeout: 5_000 })
  82. await saveBtn.click()
  83. // Document should appear in the list
  84. await expect(page.locator('[data-testid="doc-row"]').first()).toBeVisible({ timeout: 10_000 })
  85. // ── 5. Delete the collection (cleanup) ────────────────────────────────────
  86. await sidebar.getByRole('link', { name: 'Collections' }).click()
  87. await expect(page).toHaveURL(/\/collections/)
  88. // Click Delete button for e2e_docs row
  89. await page.getByRole('button', { name: 'Delete e2e_docs' }).click()
  90. // Confirm deletion
  91. await page.locator('[data-testid="confirm-dialog-confirm-btn"]').click()
  92. // Collection should be gone
  93. await expect(page.locator('[data-testid="collection-row-e2e_docs"]')).not.toBeVisible({ timeout: 10_000 })
  94. // ── 6. Logout ──────────────────────────────────────────────────────────────
  95. await page.locator('[data-testid="logout-btn"]').click()
  96. await expect(page).toHaveURL(/\/login/)
  97. })