|
| 1 | +import { test, expect, Page } from '@playwright/test'; |
| 2 | + |
| 3 | +// Smoke test for the TinaCMS admin shell at /admin/index.html. |
| 4 | +// |
| 5 | +// Catches the two regressions we hit during the 3.4 -> 3.8 upgrade: |
| 6 | +// |
| 7 | +// 1. @tinacms/cli@2.1.8+ init hang -- if `tinacms dev` never reaches |
| 8 | +// its "Dev Server is active" state, `ng serve` is never spawned and |
| 9 | +// the page never loads at all (connection refused on :4200). |
| 10 | +// |
| 11 | +// 2. `tinacms dev` binds only to ::1:4001 -- the admin's hardcoded |
| 12 | +// `http://localhost:4001/...` script tags can't resolve via the |
| 13 | +// IPv4-only docker port-publish. Tina then renders its built-in |
| 14 | +// "Failed loading TinaCMS assets" placeholder. The IPv4 forwarder |
| 15 | +// at projects/website-angular/src/scripts/tina-ipv4-proxy.js must |
| 16 | +// be running for the assets to load. |
| 17 | +// |
| 18 | +// Note on the "Enter Edit Mode" modal: a fresh playwright context has |
| 19 | +// no Tina session cookie, so Tina shows a one-time modal before exposing |
| 20 | +// the sidebar/collections. A real human only sees this once. |
| 21 | +async function dismissEditModeModal(page: Page) { |
| 22 | + // Wait for the modal to actually render (networkidle fires before Tina's |
| 23 | + // React app mounts it). Short timeout because if no modal appears, we |
| 24 | + // were already past it. |
| 25 | + const enterEdit = page.getByRole('button', { name: /enter edit mode/i }); |
| 26 | + try { |
| 27 | + await enterEdit.waitFor({ state: 'visible', timeout: 5000 }); |
| 28 | + await enterEdit.click(); |
| 29 | + await page.waitForTimeout(1500); |
| 30 | + } catch { |
| 31 | + // No modal -- already in edit mode. |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +async function enterAdmin(page: Page) { |
| 36 | + await page.goto('/admin/index.html', { waitUntil: 'networkidle' }); |
| 37 | + await dismissEditModeModal(page); |
| 38 | +} |
| 39 | + |
| 40 | +test.describe('TinaCMS admin shell', () => { |
| 41 | + test('loads without the "Failed loading assets" placeholder', async ({ page }) => { |
| 42 | + await page.goto('/admin/index.html', { waitUntil: 'networkidle' }); |
| 43 | + await expect(page).toHaveTitle(/TinaCMS/i); |
| 44 | + await expect(page.locator('#no-assets-placeholder')).toHaveCount(0); |
| 45 | + await expect(page.locator('text=Failed loading TinaCMS assets')).toHaveCount(0); |
| 46 | + }); |
| 47 | + |
| 48 | + test('every collection in tina/config.ts is reachable by URL', async ({ page }) => { |
| 49 | + // Driving Tina's collapsible sidebar via the hamburger is fragile (the |
| 50 | + // hamburger is layered behind the llama logo SVG which intercepts |
| 51 | + // pointer events). Hitting each collection's hash URL exercises the |
| 52 | + // same code path -- if Tina rejects the URL the page header label |
| 53 | + // doesn't render. This catches schema regressions (collection |
| 54 | + // removed, renamed, or broken by a config change). |
| 55 | + await page.setViewportSize({ width: 1440, height: 900 }); |
| 56 | + await enterAdmin(page); |
| 57 | + |
| 58 | + for (const { name, label } of [ |
| 59 | + { name: 'about', label: 'About' }, |
| 60 | + { name: 'news', label: 'News' }, |
| 61 | + { name: 'content', label: 'Content' }, |
| 62 | + { name: 'reactome_research_spotlights', label: 'Reactome Research Spotlights' }, |
| 63 | + { name: 'documentation', label: 'Documentation' }, |
| 64 | + { name: 'community', label: 'Community' }, |
| 65 | + ]) { |
| 66 | + await page.evaluate((n) => { |
| 67 | + window.location.hash = `#/collections/${n}`; |
| 68 | + }, name); |
| 69 | + await page.waitForTimeout(1500); |
| 70 | + await expect(page.getByRole('heading', { name: label, exact: true })).toBeVisible({ |
| 71 | + timeout: 10000, |
| 72 | + }); |
| 73 | + } |
| 74 | + }); |
| 75 | + |
| 76 | + test('clicking a row title opens the form-only admin editor, not visual edit', async ({ |
| 77 | + page, |
| 78 | + }) => { |
| 79 | + // Tina's collection table needs horizontal room. The default 1280x720 |
| 80 | + // viewport renders the table in a layout where rows don't materialize |
| 81 | + // until enough columns fit. |
| 82 | + await page.setViewportSize({ width: 1440, height: 900 }); |
| 83 | + await enterAdmin(page); |
| 84 | + |
| 85 | + // News is the worst case for visual editing: long filenames hide the |
| 86 | + // kebab "Edit in Admin" action off-screen. With `ui.router` removed |
| 87 | + // (commit 4f2c9c8 prior), the title click instead routes to the |
| 88 | + // form-only admin editor. Re-adding ui.router would re-introduce the |
| 89 | + // visual-edit iframe trap and this assertion would fail. |
| 90 | + // Don't `page.goto` to a new URL -- it triggers a full reload that |
| 91 | + // re-renders the edit-mode modal even after we dismissed it in |
| 92 | + // enterAdmin. Mutating location.hash keeps Tina's React app mounted |
| 93 | + // and just routes within it. |
| 94 | + await page.evaluate(() => { |
| 95 | + window.location.hash = '#/collections/news'; |
| 96 | + }); |
| 97 | + await page.waitForTimeout(2500); |
| 98 | + const firstRowLink = page.locator('tbody tr a').first(); |
| 99 | + await expect(firstRowLink).toBeVisible({ timeout: 15000 }); |
| 100 | + await firstRowLink.click(); |
| 101 | + |
| 102 | + await expect(page).toHaveURL(/\/admin\/index\.html#\/collections\/edit\/news\//, { |
| 103 | + timeout: 10000, |
| 104 | + }); |
| 105 | + }); |
| 106 | +}); |
0 commit comments